@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.
- package/README.md +82 -39
- package/next.config.ts +31 -6
- package/package.json +3 -2
- package/src/app/api/agents/[id]/thread/route.ts +1 -0
- package/src/app/api/agents/route.ts +19 -5
- package/src/app/api/approvals/route.ts +22 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/eval/run/route.ts +37 -0
- package/src/app/api/eval/scenarios/route.ts +24 -0
- package/src/app/api/eval/suite/route.ts +29 -0
- package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
- package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
- package/src/app/api/memory/graph/route.ts +46 -0
- package/src/app/api/memory/route.ts +36 -5
- package/src/app/api/notifications/route.ts +3 -0
- package/src/app/api/plugins/install/route.ts +57 -5
- package/src/app/api/plugins/marketplace/route.ts +73 -22
- package/src/app/api/plugins/route.ts +61 -1
- package/src/app/api/plugins/ui/route.ts +34 -0
- package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
- package/src/app/api/sessions/[id]/restore/route.ts +36 -0
- package/src/app/api/settings/route.ts +62 -0
- package/src/app/api/setup/doctor/route.ts +22 -5
- package/src/app/api/souls/[id]/route.ts +65 -0
- package/src/app/api/souls/route.ts +70 -0
- package/src/app/api/tasks/[id]/approve/route.ts +4 -3
- package/src/app/api/tasks/[id]/route.ts +16 -3
- package/src/app/api/tasks/route.ts +10 -2
- package/src/app/api/usage/route.ts +9 -2
- package/src/app/globals.css +27 -0
- package/src/app/page.tsx +10 -5
- package/src/cli/index.js +37 -0
- package/src/components/activity/activity-feed.tsx +9 -2
- package/src/components/agents/agent-avatar.tsx +5 -1
- package/src/components/agents/agent-card.tsx +55 -9
- package/src/components/agents/agent-sheet.tsx +112 -34
- package/src/components/agents/inspector-panel.tsx +1 -1
- package/src/components/agents/soul-library-picker.tsx +84 -13
- package/src/components/auth/access-key-gate.tsx +63 -54
- package/src/components/auth/user-picker.tsx +37 -32
- package/src/components/chat/activity-moment.tsx +2 -0
- package/src/components/chat/chat-area.tsx +11 -0
- package/src/components/chat/chat-header.tsx +69 -25
- package/src/components/chat/chat-tool-toggles.tsx +2 -2
- package/src/components/chat/checkpoint-timeline.tsx +112 -0
- package/src/components/chat/code-block.tsx +3 -1
- package/src/components/chat/exec-approval-card.tsx +8 -1
- package/src/components/chat/message-bubble.tsx +164 -4
- package/src/components/chat/message-list.tsx +46 -4
- package/src/components/chat/session-approval-card.tsx +80 -0
- package/src/components/chat/session-debug-panel.tsx +106 -84
- package/src/components/chat/streaming-bubble.tsx +6 -5
- package/src/components/chat/task-approval-card.tsx +78 -0
- package/src/components/chat/thinking-indicator.tsx +48 -12
- package/src/components/chat/tool-call-bubble.tsx +3 -0
- package/src/components/chat/tool-request-banner.tsx +39 -20
- package/src/components/chatrooms/chatroom-list.tsx +11 -4
- package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
- package/src/components/connectors/connector-list.tsx +33 -11
- package/src/components/connectors/connector-sheet.tsx +37 -7
- package/src/components/home/home-view.tsx +54 -24
- package/src/components/input/chat-input.tsx +22 -1
- package/src/components/knowledge/knowledge-list.tsx +17 -18
- package/src/components/knowledge/knowledge-sheet.tsx +9 -5
- package/src/components/layout/app-layout.tsx +87 -19
- package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
- package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
- package/src/components/memory/memory-browser.tsx +73 -45
- package/src/components/memory/memory-graph-view.tsx +203 -0
- package/src/components/memory/memory-list.tsx +20 -13
- package/src/components/plugins/plugin-list.tsx +214 -60
- package/src/components/plugins/plugin-sheet.tsx +119 -24
- package/src/components/projects/project-list.tsx +17 -9
- package/src/components/providers/provider-list.tsx +21 -6
- package/src/components/providers/provider-sheet.tsx +42 -25
- package/src/components/runs/run-list.tsx +17 -13
- package/src/components/schedules/schedule-card.tsx +10 -3
- package/src/components/schedules/schedule-list.tsx +2 -2
- package/src/components/schedules/schedule-sheet.tsx +28 -9
- package/src/components/secrets/secret-sheet.tsx +7 -2
- package/src/components/secrets/secrets-list.tsx +18 -5
- package/src/components/sessions/new-session-sheet.tsx +183 -376
- package/src/components/sessions/session-card.tsx +10 -2
- package/src/components/settings/gateway-connection-panel.tsx +9 -8
- package/src/components/shared/command-palette.tsx +13 -5
- package/src/components/shared/empty-state.tsx +20 -8
- package/src/components/shared/hint-tip.tsx +31 -0
- package/src/components/shared/notification-center.tsx +134 -86
- package/src/components/shared/profile-sheet.tsx +4 -0
- package/src/components/shared/settings/plugin-manager.tsx +360 -135
- package/src/components/shared/settings/section-capability-policy.tsx +3 -3
- package/src/components/shared/settings/section-runtime-loop.tsx +149 -4
- package/src/components/skills/clawhub-browser.tsx +1 -0
- package/src/components/skills/skill-list.tsx +31 -12
- package/src/components/skills/skill-sheet.tsx +20 -7
- package/src/components/tasks/approvals-panel.tsx +224 -0
- package/src/components/tasks/task-board.tsx +20 -12
- package/src/components/tasks/task-card.tsx +21 -7
- package/src/components/tasks/task-column.tsx +4 -3
- package/src/components/tasks/task-list.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +130 -1
- package/src/components/ui/dialog.tsx +1 -0
- package/src/components/ui/sheet.tsx +1 -0
- package/src/components/usage/metrics-dashboard.tsx +72 -48
- package/src/components/wallets/wallet-panel.tsx +65 -41
- package/src/components/wallets/wallet-section.tsx +9 -3
- package/src/components/webhooks/webhook-list.tsx +21 -12
- package/src/components/webhooks/webhook-sheet.tsx +13 -3
- package/src/lib/approval-display.test.ts +45 -0
- package/src/lib/approval-display.ts +62 -0
- package/src/lib/clipboard.ts +38 -0
- package/src/lib/memory.ts +8 -0
- package/src/lib/providers/claude-cli.ts +5 -3
- package/src/lib/providers/index.ts +67 -21
- package/src/lib/runtime-loop.ts +3 -2
- package/src/lib/server/approvals.ts +150 -0
- package/src/lib/server/chat-execution.ts +319 -74
- package/src/lib/server/chatroom-helpers.ts +63 -5
- package/src/lib/server/chatroom-orchestration.ts +74 -0
- package/src/lib/server/clawhub-client.ts +82 -6
- package/src/lib/server/connectors/manager.ts +27 -1
- package/src/lib/server/context-manager.ts +132 -50
- package/src/lib/server/cost.test.ts +73 -0
- package/src/lib/server/cost.ts +165 -34
- package/src/lib/server/daemon-state.ts +112 -1
- package/src/lib/server/data-dir.ts +18 -1
- package/src/lib/server/eval/runner.ts +126 -0
- package/src/lib/server/eval/scenarios.ts +218 -0
- package/src/lib/server/eval/scorer.ts +96 -0
- package/src/lib/server/eval/store.ts +37 -0
- package/src/lib/server/eval/types.ts +48 -0
- package/src/lib/server/execution-log.ts +12 -8
- package/src/lib/server/guardian.ts +34 -0
- package/src/lib/server/heartbeat-service.ts +53 -1
- package/src/lib/server/integrity-monitor.ts +208 -0
- package/src/lib/server/langgraph-checkpoint.ts +10 -0
- package/src/lib/server/link-understanding.ts +55 -0
- package/src/lib/server/llm-response-cache.test.ts +102 -0
- package/src/lib/server/llm-response-cache.ts +227 -0
- package/src/lib/server/main-agent-loop.ts +115 -16
- package/src/lib/server/main-session.ts +6 -3
- package/src/lib/server/mcp-conformance.test.ts +18 -0
- package/src/lib/server/mcp-conformance.ts +233 -0
- package/src/lib/server/memory-db.ts +193 -19
- package/src/lib/server/memory-retrieval.test.ts +56 -0
- package/src/lib/server/mmr.ts +73 -0
- package/src/lib/server/orchestrator-lg.ts +7 -1
- package/src/lib/server/orchestrator.ts +4 -3
- package/src/lib/server/plugins.ts +662 -132
- package/src/lib/server/process-manager.ts +18 -0
- package/src/lib/server/query-expansion.ts +57 -0
- package/src/lib/server/queue.ts +280 -11
- package/src/lib/server/runtime-settings.ts +9 -0
- package/src/lib/server/session-run-manager.test.ts +23 -0
- package/src/lib/server/session-run-manager.ts +32 -2
- package/src/lib/server/session-tools/canvas.ts +85 -50
- package/src/lib/server/session-tools/chatroom.ts +130 -127
- package/src/lib/server/session-tools/connector.ts +233 -454
- package/src/lib/server/session-tools/context-mgmt.ts +87 -105
- package/src/lib/server/session-tools/crud.ts +84 -7
- package/src/lib/server/session-tools/delegate.ts +351 -752
- package/src/lib/server/session-tools/discovery.ts +198 -0
- package/src/lib/server/session-tools/edit_file.ts +82 -0
- package/src/lib/server/session-tools/file-send.test.ts +39 -0
- package/src/lib/server/session-tools/file.ts +257 -425
- package/src/lib/server/session-tools/git.ts +87 -47
- package/src/lib/server/session-tools/http.ts +95 -33
- package/src/lib/server/session-tools/index.ts +217 -138
- package/src/lib/server/session-tools/memory.ts +154 -239
- package/src/lib/server/session-tools/monitor.ts +126 -0
- package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
- package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
- package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
- package/src/lib/server/session-tools/platform.ts +86 -0
- package/src/lib/server/session-tools/plugin-creator.ts +239 -0
- package/src/lib/server/session-tools/sample-ui.ts +97 -0
- package/src/lib/server/session-tools/sandbox.ts +175 -148
- package/src/lib/server/session-tools/schedule.ts +78 -0
- package/src/lib/server/session-tools/session-info.ts +104 -410
- package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
- package/src/lib/server/session-tools/shell.ts +171 -143
- package/src/lib/server/session-tools/subagent.ts +77 -77
- package/src/lib/server/session-tools/wallet.ts +182 -106
- package/src/lib/server/session-tools/web.ts +181 -327
- package/src/lib/server/storage.ts +36 -0
- package/src/lib/server/stream-agent-chat.ts +348 -242
- package/src/lib/server/task-quality-gate.test.ts +44 -0
- package/src/lib/server/task-quality-gate.ts +67 -0
- package/src/lib/server/task-validation.test.ts +78 -0
- package/src/lib/server/task-validation.ts +67 -2
- package/src/lib/server/tool-aliases.ts +68 -0
- package/src/lib/server/tool-capability-policy.ts +24 -5
- package/src/lib/server/tool-retry.ts +62 -0
- package/src/lib/server/transcript-repair.ts +72 -0
- package/src/lib/setup-defaults.ts +1 -0
- package/src/lib/tasks.ts +7 -1
- package/src/lib/tool-definitions.ts +24 -23
- package/src/lib/validation/schemas.ts +13 -0
- package/src/lib/view-routes.ts +2 -23
- package/src/stores/use-app-store.ts +23 -1
- 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 (
|
|
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
|
-
|
|
125
|
-
|
|
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:
|
|
158
|
-
schema: z.object({})
|
|
159
|
-
}
|
|
160
|
-
)
|
|
193
|
+
description: SandboxPlugin.tools![1].description,
|
|
194
|
+
schema: z.object({}).passthrough()
|
|
195
|
+
}
|
|
196
|
+
)
|
|
161
197
|
)
|
|
162
198
|
|
|
163
|
-
|
|
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 (
|
|
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(
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
187
|
-
schema: z.object({
|
|
188
|
-
|
|
189
|
-
|
|
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
|
+
}
|