@swarmclawai/swarmclaw 0.6.8 → 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 (166) hide show
  1. package/README.md +70 -45
  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 +18 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/clawhub/install/route.ts +2 -2
  8. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  9. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  10. package/src/app/api/memory/route.ts +36 -5
  11. package/src/app/api/notifications/route.ts +3 -0
  12. package/src/app/api/plugins/install/route.ts +57 -5
  13. package/src/app/api/plugins/marketplace/route.ts +73 -22
  14. package/src/app/api/plugins/route.ts +61 -1
  15. package/src/app/api/plugins/ui/route.ts +34 -0
  16. package/src/app/api/settings/route.ts +62 -0
  17. package/src/app/api/setup/doctor/route.ts +22 -5
  18. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  19. package/src/app/api/tasks/[id]/route.ts +11 -3
  20. package/src/app/api/tasks/route.ts +8 -2
  21. package/src/app/globals.css +27 -0
  22. package/src/app/page.tsx +10 -5
  23. package/src/cli/index.js +13 -0
  24. package/src/components/activity/activity-feed.tsx +9 -2
  25. package/src/components/agents/agent-avatar.tsx +5 -1
  26. package/src/components/agents/agent-card.tsx +55 -9
  27. package/src/components/agents/agent-sheet.tsx +86 -29
  28. package/src/components/agents/inspector-panel.tsx +1 -1
  29. package/src/components/auth/access-key-gate.tsx +63 -54
  30. package/src/components/auth/user-picker.tsx +37 -32
  31. package/src/components/chat/chat-area.tsx +11 -0
  32. package/src/components/chat/chat-header.tsx +69 -25
  33. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  34. package/src/components/chat/code-block.tsx +3 -1
  35. package/src/components/chat/exec-approval-card.tsx +8 -1
  36. package/src/components/chat/message-bubble.tsx +164 -4
  37. package/src/components/chat/message-list.tsx +30 -4
  38. package/src/components/chat/session-approval-card.tsx +80 -0
  39. package/src/components/chat/streaming-bubble.tsx +6 -5
  40. package/src/components/chat/thinking-indicator.tsx +48 -12
  41. package/src/components/chat/tool-request-banner.tsx +39 -20
  42. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  43. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  44. package/src/components/connectors/connector-list.tsx +33 -11
  45. package/src/components/connectors/connector-sheet.tsx +29 -6
  46. package/src/components/home/home-view.tsx +20 -14
  47. package/src/components/input/chat-input.tsx +22 -1
  48. package/src/components/knowledge/knowledge-list.tsx +17 -18
  49. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  50. package/src/components/layout/app-layout.tsx +73 -21
  51. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  52. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  53. package/src/components/memory/memory-list.tsx +20 -13
  54. package/src/components/plugins/plugin-list.tsx +213 -59
  55. package/src/components/plugins/plugin-sheet.tsx +119 -24
  56. package/src/components/projects/project-list.tsx +17 -9
  57. package/src/components/providers/provider-list.tsx +21 -6
  58. package/src/components/providers/provider-sheet.tsx +42 -25
  59. package/src/components/runs/run-list.tsx +17 -13
  60. package/src/components/schedules/schedule-card.tsx +10 -3
  61. package/src/components/schedules/schedule-list.tsx +2 -2
  62. package/src/components/schedules/schedule-sheet.tsx +19 -7
  63. package/src/components/secrets/secret-sheet.tsx +7 -2
  64. package/src/components/secrets/secrets-list.tsx +18 -5
  65. package/src/components/sessions/new-session-sheet.tsx +183 -376
  66. package/src/components/sessions/session-card.tsx +10 -2
  67. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  68. package/src/components/shared/command-palette.tsx +13 -5
  69. package/src/components/shared/empty-state.tsx +20 -8
  70. package/src/components/shared/notification-center.tsx +134 -86
  71. package/src/components/shared/profile-sheet.tsx +4 -0
  72. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  73. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  74. package/src/components/shared/settings/section-runtime-loop.tsx +144 -0
  75. package/src/components/skills/clawhub-browser.tsx +1 -0
  76. package/src/components/skills/skill-list.tsx +31 -12
  77. package/src/components/skills/skill-sheet.tsx +20 -7
  78. package/src/components/tasks/approvals-panel.tsx +170 -66
  79. package/src/components/tasks/task-board.tsx +20 -12
  80. package/src/components/tasks/task-card.tsx +21 -7
  81. package/src/components/tasks/task-column.tsx +4 -3
  82. package/src/components/tasks/task-list.tsx +1 -1
  83. package/src/components/tasks/task-sheet.tsx +130 -1
  84. package/src/components/ui/dialog.tsx +1 -0
  85. package/src/components/ui/sheet.tsx +1 -0
  86. package/src/components/usage/metrics-dashboard.tsx +66 -64
  87. package/src/components/wallets/wallet-panel.tsx +65 -41
  88. package/src/components/wallets/wallet-section.tsx +9 -3
  89. package/src/components/webhooks/webhook-list.tsx +21 -12
  90. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  91. package/src/lib/approval-display.test.ts +45 -0
  92. package/src/lib/approval-display.ts +62 -0
  93. package/src/lib/clipboard.ts +38 -0
  94. package/src/lib/memory.ts +8 -0
  95. package/src/lib/providers/claude-cli.ts +5 -3
  96. package/src/lib/providers/index.ts +67 -21
  97. package/src/lib/runtime-loop.ts +3 -2
  98. package/src/lib/server/approvals.ts +150 -0
  99. package/src/lib/server/chat-execution.ts +223 -62
  100. package/src/lib/server/clawhub-client.ts +82 -6
  101. package/src/lib/server/connectors/manager.ts +27 -1
  102. package/src/lib/server/cost.test.ts +73 -0
  103. package/src/lib/server/cost.ts +165 -34
  104. package/src/lib/server/daemon-state.ts +42 -0
  105. package/src/lib/server/data-dir.ts +18 -1
  106. package/src/lib/server/integrity-monitor.ts +208 -0
  107. package/src/lib/server/llm-response-cache.test.ts +102 -0
  108. package/src/lib/server/llm-response-cache.ts +227 -0
  109. package/src/lib/server/main-agent-loop.ts +1 -1
  110. package/src/lib/server/main-session.ts +6 -3
  111. package/src/lib/server/mcp-conformance.test.ts +18 -0
  112. package/src/lib/server/mcp-conformance.ts +233 -0
  113. package/src/lib/server/memory-db.ts +180 -17
  114. package/src/lib/server/memory-retrieval.test.ts +56 -0
  115. package/src/lib/server/orchestrator-lg.ts +4 -1
  116. package/src/lib/server/orchestrator.ts +4 -3
  117. package/src/lib/server/plugins.ts +650 -142
  118. package/src/lib/server/process-manager.ts +18 -0
  119. package/src/lib/server/queue.ts +253 -11
  120. package/src/lib/server/runtime-settings.ts +9 -0
  121. package/src/lib/server/session-run-manager.test.ts +23 -0
  122. package/src/lib/server/session-run-manager.ts +11 -1
  123. package/src/lib/server/session-tools/canvas.ts +85 -50
  124. package/src/lib/server/session-tools/chatroom.ts +130 -127
  125. package/src/lib/server/session-tools/connector.ts +233 -454
  126. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  127. package/src/lib/server/session-tools/crud.ts +84 -7
  128. package/src/lib/server/session-tools/delegate.ts +351 -752
  129. package/src/lib/server/session-tools/discovery.ts +198 -0
  130. package/src/lib/server/session-tools/edit_file.ts +82 -0
  131. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  132. package/src/lib/server/session-tools/file.ts +257 -425
  133. package/src/lib/server/session-tools/git.ts +87 -47
  134. package/src/lib/server/session-tools/http.ts +85 -33
  135. package/src/lib/server/session-tools/index.ts +205 -160
  136. package/src/lib/server/session-tools/memory.ts +152 -265
  137. package/src/lib/server/session-tools/monitor.ts +126 -0
  138. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  139. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  140. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  141. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  142. package/src/lib/server/session-tools/platform.ts +86 -0
  143. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  144. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  145. package/src/lib/server/session-tools/sandbox.ts +175 -148
  146. package/src/lib/server/session-tools/schedule.ts +66 -31
  147. package/src/lib/server/session-tools/session-info.ts +104 -410
  148. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  149. package/src/lib/server/session-tools/shell.ts +171 -143
  150. package/src/lib/server/session-tools/subagent.ts +77 -77
  151. package/src/lib/server/session-tools/wallet.ts +182 -106
  152. package/src/lib/server/session-tools/web.ts +179 -349
  153. package/src/lib/server/storage.ts +24 -0
  154. package/src/lib/server/stream-agent-chat.ts +301 -244
  155. package/src/lib/server/task-quality-gate.test.ts +44 -0
  156. package/src/lib/server/task-quality-gate.ts +67 -0
  157. package/src/lib/server/task-validation.test.ts +78 -0
  158. package/src/lib/server/task-validation.ts +67 -2
  159. package/src/lib/server/tool-aliases.ts +68 -0
  160. package/src/lib/server/tool-capability-policy.ts +23 -5
  161. package/src/lib/tasks.ts +7 -1
  162. package/src/lib/tool-definitions.ts +23 -23
  163. package/src/lib/validation/schemas.ts +12 -0
  164. package/src/lib/view-routes.ts +2 -24
  165. package/src/stores/use-app-store.ts +23 -1
  166. package/src/types/index.ts +121 -7
@@ -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
 
@@ -3,41 +3,76 @@ import { z } from 'zod'
3
3
  import { enqueueSystemEvent } from '../system-events'
4
4
  import { requestHeartbeatNow } from '../heartbeat-wake'
5
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'
6
9
 
7
- export function buildScheduleTools(bctx: ToolBuildContext): StructuredToolInterface[] {
8
- const tools: StructuredToolInterface[] = []
9
- const { ctx, hasTool } = bctx
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).'
10
19
 
11
- if (hasTool('schedule_wake')) {
12
- tools.push(
13
- tool(
14
- async ({ delayMinutes, message }) => {
15
- if (!ctx?.sessionId) return 'Cannot schedule wake: no session context.'
16
- if (delayMinutes <= 0 || delayMinutes > 1440) return 'delayMinutes must be between 1 and 1440 (24 hours).'
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
+ }
17
25
 
18
- // Non-durable in-memory timeout for conversational wake events
19
- // (For durable cron, use manage_schedules)
20
- const delayMs = delayMinutes * 60 * 1000
21
- setTimeout(() => {
22
- if (ctx.sessionId) {
23
- enqueueSystemEvent(ctx.sessionId, `[Scheduled Wake Event / Reminder] ${message}`)
24
- requestHeartbeatNow({ sessionId: ctx.sessionId, reason: 'scheduled_wake' })
25
- }
26
- }, delayMs)
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)
27
33
 
28
- return `Successfully scheduled a wake event in ${delayMinutes} minutes with message: "${message}".`
29
- },
30
- {
31
- name: 'schedule_wake',
32
- description: 'Schedule a wake event (reminder) for yourself in this chatroom. Use this to proactively check back on a long-running process or to remind yourself to follow up with the user later.',
33
- schema: z.object({
34
- delayMinutes: z.number().describe('How many minutes from now to wake up (1-1440).'),
35
- message: z.string().describe('The reminder text that will be passed back to you when you wake.'),
36
- }),
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' }
37
53
  },
38
- ),
39
- )
40
- }
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)
41
62
 
42
- return tools
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
+ ]
43
78
  }