@hitechclaw/clawspark 2.0.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/CHANGELOG.md +35 -0
- package/LICENSE +21 -0
- package/README.md +378 -0
- package/clawspark +2715 -0
- package/configs/models.yaml +108 -0
- package/configs/skill-packs.yaml +44 -0
- package/configs/skills.yaml +37 -0
- package/install.sh +387 -0
- package/lib/common.sh +249 -0
- package/lib/detect-hardware.sh +156 -0
- package/lib/diagnose.sh +636 -0
- package/lib/render-diagram.sh +47 -0
- package/lib/sandbox-commands.sh +415 -0
- package/lib/secure.sh +244 -0
- package/lib/select-model.sh +442 -0
- package/lib/setup-browser.sh +138 -0
- package/lib/setup-dashboard.sh +228 -0
- package/lib/setup-inference.sh +128 -0
- package/lib/setup-mcp.sh +142 -0
- package/lib/setup-messaging.sh +242 -0
- package/lib/setup-models.sh +121 -0
- package/lib/setup-openclaw.sh +808 -0
- package/lib/setup-sandbox.sh +188 -0
- package/lib/setup-skills.sh +113 -0
- package/lib/setup-systemd.sh +224 -0
- package/lib/setup-tailscale.sh +188 -0
- package/lib/setup-voice.sh +101 -0
- package/lib/skill-audit.sh +449 -0
- package/lib/verify.sh +177 -0
- package/package.json +57 -0
- package/scripts/release.sh +133 -0
- package/uninstall.sh +161 -0
- package/v2/README.md +50 -0
- package/v2/configs/providers.yaml +79 -0
- package/v2/configs/skills.yaml +36 -0
- package/v2/install.sh +116 -0
- package/v2/lib/common.sh +285 -0
- package/v2/lib/detect-hardware.sh +119 -0
- package/v2/lib/select-runtime.sh +273 -0
- package/v2/lib/setup-extras.sh +95 -0
- package/v2/lib/setup-openclaw.sh +187 -0
- package/v2/lib/setup-provider.sh +131 -0
- package/v2/lib/verify.sh +133 -0
- package/web/index.html +1835 -0
- package/web/install.sh +387 -0
- package/web/logo-hero.svg +11 -0
- package/web/logo-icon.svg +12 -0
- package/web/logo.svg +17 -0
- package/web/vercel.json +8 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# lib/sandbox-commands.sh -- CLI subcommands for managing the Docker sandbox.
|
|
3
|
+
# Sourced by the clawspark CLI. Provides: clawspark sandbox [on|off|status|test]
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
_cmd_sandbox() {
|
|
7
|
+
local subcmd="${1:-status}"
|
|
8
|
+
shift || true
|
|
9
|
+
|
|
10
|
+
case "${subcmd}" in
|
|
11
|
+
on) _sandbox_on ;;
|
|
12
|
+
off) _sandbox_off ;;
|
|
13
|
+
status) _sandbox_status ;;
|
|
14
|
+
test) _sandbox_test ;;
|
|
15
|
+
*)
|
|
16
|
+
log_error "Unknown sandbox subcommand: ${subcmd}"
|
|
17
|
+
printf ' Usage: clawspark sandbox [on|off|status|test]\n'
|
|
18
|
+
exit 1
|
|
19
|
+
;;
|
|
20
|
+
esac
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# ── sandbox on ────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
_sandbox_on() {
|
|
26
|
+
local config_file="${HOME}/.openclaw/openclaw.json"
|
|
27
|
+
local sandbox_dir="${CLAWSPARK_DIR}/sandbox"
|
|
28
|
+
|
|
29
|
+
if [[ ! -f "${config_file}" ]]; then
|
|
30
|
+
log_error "Config not found at ${config_file}. Run install.sh first."
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Check Docker availability
|
|
35
|
+
if ! check_command docker; then
|
|
36
|
+
log_error "Docker is not installed. Install Docker first to use the sandbox."
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
if ! docker info &>/dev/null; then
|
|
41
|
+
log_error "Docker daemon is not running. Start Docker first."
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# Build the sandbox image if it does not exist
|
|
46
|
+
if ! docker image inspect clawspark-sandbox:latest &>/dev/null; then
|
|
47
|
+
if [[ -f "${sandbox_dir}/Dockerfile" ]]; then
|
|
48
|
+
log_info "Sandbox image not found. Building it now..."
|
|
49
|
+
docker build -t clawspark-sandbox:latest "${sandbox_dir}" 2>&1 \
|
|
50
|
+
| tee -a "${CLAWSPARK_LOG}"
|
|
51
|
+
if ! docker image inspect clawspark-sandbox:latest &>/dev/null; then
|
|
52
|
+
log_error "Image build failed. Check ${CLAWSPARK_LOG}."
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
log_success "Sandbox image built."
|
|
56
|
+
else
|
|
57
|
+
log_error "Sandbox Dockerfile not found at ${sandbox_dir}/Dockerfile."
|
|
58
|
+
log_error "Re-run install.sh to set up the sandbox."
|
|
59
|
+
exit 1
|
|
60
|
+
fi
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# Enable sandbox using the correct OpenClaw schema path: agents.defaults.sandbox
|
|
64
|
+
# NOTE: Do NOT set network:"none" here -- it breaks the main agent's network access.
|
|
65
|
+
# Network isolation is handled by the Docker --network=none flag in run.sh instead.
|
|
66
|
+
python3 -c "
|
|
67
|
+
import json, sys
|
|
68
|
+
|
|
69
|
+
path = sys.argv[1]
|
|
70
|
+
|
|
71
|
+
with open(path, 'r') as f:
|
|
72
|
+
cfg = json.load(f)
|
|
73
|
+
|
|
74
|
+
cfg.setdefault('agents', {}).setdefault('defaults', {})
|
|
75
|
+
cfg['agents']['defaults']['sandbox'] = {
|
|
76
|
+
'mode': 'non-main',
|
|
77
|
+
'scope': 'session',
|
|
78
|
+
'docker': {
|
|
79
|
+
'image': 'clawspark-sandbox:latest'
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Clean up any invalid root-level sandbox key from previous installs
|
|
84
|
+
cfg.pop('sandbox', None)
|
|
85
|
+
|
|
86
|
+
with open(path, 'w') as f:
|
|
87
|
+
json.dump(cfg, f, indent=2)
|
|
88
|
+
print('ok')
|
|
89
|
+
" "${config_file}" 2>> "${CLAWSPARK_LOG}" || {
|
|
90
|
+
log_error "Failed to update openclaw.json."
|
|
91
|
+
exit 1
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Persist sandbox state
|
|
95
|
+
echo "true" > "${CLAWSPARK_DIR}/sandbox.state"
|
|
96
|
+
|
|
97
|
+
log_success "Sandbox enabled. Sub-agent code execution runs in Docker."
|
|
98
|
+
log_info "Restart to apply: clawspark restart"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# ── sandbox off ───────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
_sandbox_off() {
|
|
104
|
+
local config_file="${HOME}/.openclaw/openclaw.json"
|
|
105
|
+
|
|
106
|
+
if [[ -f "${config_file}" ]]; then
|
|
107
|
+
python3 -c "
|
|
108
|
+
import json, sys
|
|
109
|
+
|
|
110
|
+
path = sys.argv[1]
|
|
111
|
+
|
|
112
|
+
with open(path, 'r') as f:
|
|
113
|
+
cfg = json.load(f)
|
|
114
|
+
|
|
115
|
+
# Remove sandbox from the correct schema path
|
|
116
|
+
if 'agents' in cfg and 'defaults' in cfg['agents']:
|
|
117
|
+
cfg['agents']['defaults'].pop('sandbox', None)
|
|
118
|
+
|
|
119
|
+
# Also clean up any invalid root-level key
|
|
120
|
+
cfg.pop('sandbox', None)
|
|
121
|
+
|
|
122
|
+
with open(path, 'w') as f:
|
|
123
|
+
json.dump(cfg, f, indent=2)
|
|
124
|
+
print('ok')
|
|
125
|
+
" "${config_file}" 2>> "${CLAWSPARK_LOG}" || true
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
# Persist sandbox state
|
|
129
|
+
echo "false" > "${CLAWSPARK_DIR}/sandbox.state"
|
|
130
|
+
|
|
131
|
+
log_success "Sandbox disabled. Code execution will run on the host."
|
|
132
|
+
log_info "Restart to apply: clawspark restart"
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# ── sandbox status ────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
_sandbox_status() {
|
|
138
|
+
printf '\n%s%s clawspark sandbox status%s\n\n' "${BOLD}" "${BLUE}" "${RESET}"
|
|
139
|
+
|
|
140
|
+
# Docker installed?
|
|
141
|
+
if check_command docker; then
|
|
142
|
+
printf ' %s✓%s Docker installed\n' "${GREEN}" "${RESET}"
|
|
143
|
+
else
|
|
144
|
+
printf ' %s✗%s Docker not installed\n' "${RED}" "${RESET}"
|
|
145
|
+
printf '\n Install Docker to enable sandbox support.\n\n'
|
|
146
|
+
return
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
# Docker daemon running?
|
|
150
|
+
if docker info &>/dev/null; then
|
|
151
|
+
printf ' %s✓%s Docker daemon running\n' "${GREEN}" "${RESET}"
|
|
152
|
+
else
|
|
153
|
+
printf ' %s✗%s Docker daemon not running\n' "${RED}" "${RESET}"
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
# Sandbox image exists?
|
|
157
|
+
if docker image inspect clawspark-sandbox:latest &>/dev/null; then
|
|
158
|
+
local image_size
|
|
159
|
+
image_size=$(docker image inspect clawspark-sandbox:latest \
|
|
160
|
+
--format '{{.Size}}' 2>/dev/null || echo "0")
|
|
161
|
+
# Convert bytes to MB
|
|
162
|
+
local size_mb
|
|
163
|
+
size_mb=$(awk "BEGIN {printf \"%.0f\", ${image_size} / 1048576}")
|
|
164
|
+
printf ' %s✓%s Sandbox image built (%s MB)\n' "${GREEN}" "${RESET}" "${size_mb}"
|
|
165
|
+
else
|
|
166
|
+
printf ' %s✗%s Sandbox image not built\n' "${YELLOW}" "${RESET}"
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
# Seccomp profile exists?
|
|
170
|
+
local seccomp_path="${CLAWSPARK_DIR}/sandbox/seccomp-profile.json"
|
|
171
|
+
if [[ -f "${seccomp_path}" ]]; then
|
|
172
|
+
printf ' %s✓%s Seccomp profile present\n' "${GREEN}" "${RESET}"
|
|
173
|
+
else
|
|
174
|
+
printf ' %s-%s Seccomp profile missing\n' "${YELLOW}" "${RESET}"
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
# Sandbox enabled in config?
|
|
178
|
+
local config_file="${HOME}/.openclaw/openclaw.json"
|
|
179
|
+
local sandbox_mode="not configured"
|
|
180
|
+
if [[ -f "${config_file}" ]]; then
|
|
181
|
+
sandbox_mode=$(python3 -c "
|
|
182
|
+
import json, sys
|
|
183
|
+
with open(sys.argv[1]) as f:
|
|
184
|
+
cfg = json.load(f)
|
|
185
|
+
mode = cfg.get('agents', {}).get('defaults', {}).get('sandbox', {}).get('mode', 'not configured')
|
|
186
|
+
print(mode)
|
|
187
|
+
" "${config_file}" 2>/dev/null || echo "not configured")
|
|
188
|
+
fi
|
|
189
|
+
|
|
190
|
+
if [[ "${sandbox_mode}" == "not configured" ]]; then
|
|
191
|
+
printf ' %s-%s Sandbox mode %s\n' "${YELLOW}" "${RESET}" "${sandbox_mode}"
|
|
192
|
+
else
|
|
193
|
+
printf ' %s✓%s Sandbox mode %s\n' "${GREEN}" "${RESET}" "${sandbox_mode}"
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
# Sandbox state file
|
|
197
|
+
local state="off"
|
|
198
|
+
if [[ -f "${CLAWSPARK_DIR}/sandbox.state" ]]; then
|
|
199
|
+
state=$(cat "${CLAWSPARK_DIR}/sandbox.state")
|
|
200
|
+
[[ "${state}" == "true" ]] && state="ON" || state="off"
|
|
201
|
+
fi
|
|
202
|
+
printf ' %s-%s Sandbox state %s\n' "${CYAN}" "${RESET}" "${state}"
|
|
203
|
+
|
|
204
|
+
# Security summary
|
|
205
|
+
printf '\n %s%sSecurity constraints:%s\n' "${BOLD}" "${CYAN}" "${RESET}"
|
|
206
|
+
printf ' --network=none No network access from sandbox\n'
|
|
207
|
+
printf ' --cap-drop=ALL All Linux capabilities dropped\n'
|
|
208
|
+
printf ' --read-only Root filesystem is read-only\n'
|
|
209
|
+
printf ' --pids-limit=200 Process count limited to 200\n'
|
|
210
|
+
printf ' --memory=1g Memory capped at 1 GB\n'
|
|
211
|
+
printf ' --cpus=2 CPU limited to 2 cores\n'
|
|
212
|
+
printf ' seccomp profile Dangerous syscalls blocked\n'
|
|
213
|
+
printf ' no-new-privileges Privilege escalation prevented\n'
|
|
214
|
+
|
|
215
|
+
printf '\n Enable: clawspark sandbox on\n'
|
|
216
|
+
printf ' Disable: clawspark sandbox off\n'
|
|
217
|
+
printf ' Test: clawspark sandbox test\n\n'
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# ── sandbox test ──────────────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
_sandbox_test() {
|
|
223
|
+
printf '\n%s%s clawspark sandbox test%s\n\n' "${BOLD}" "${BLUE}" "${RESET}"
|
|
224
|
+
|
|
225
|
+
# Preflight checks
|
|
226
|
+
if ! check_command docker; then
|
|
227
|
+
log_error "Docker is not installed."
|
|
228
|
+
exit 1
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
if ! docker info &>/dev/null; then
|
|
232
|
+
log_error "Docker daemon is not running."
|
|
233
|
+
exit 1
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
if ! docker image inspect clawspark-sandbox:latest &>/dev/null; then
|
|
237
|
+
log_error "Sandbox image not found. Run: clawspark sandbox on"
|
|
238
|
+
exit 1
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
local sandbox_dir="${CLAWSPARK_DIR}/sandbox"
|
|
242
|
+
local seccomp_path="${sandbox_dir}/seccomp-profile.json"
|
|
243
|
+
local seccomp_opt=""
|
|
244
|
+
if [[ -f "${seccomp_path}" ]]; then
|
|
245
|
+
seccomp_opt="--security-opt=seccomp=${seccomp_path}"
|
|
246
|
+
fi
|
|
247
|
+
|
|
248
|
+
local passed=0
|
|
249
|
+
local failed=0
|
|
250
|
+
|
|
251
|
+
# Test 1: Basic Python execution
|
|
252
|
+
printf ' %s[1/5]%s Python execution ... ' "${CYAN}" "${RESET}"
|
|
253
|
+
local result
|
|
254
|
+
result=$(docker run --rm \
|
|
255
|
+
--read-only \
|
|
256
|
+
--tmpfs /tmp:size=100m \
|
|
257
|
+
--tmpfs /sandbox/work:size=500m \
|
|
258
|
+
--network=none \
|
|
259
|
+
--cap-drop=ALL \
|
|
260
|
+
--security-opt=no-new-privileges \
|
|
261
|
+
${seccomp_opt} \
|
|
262
|
+
--memory=1g \
|
|
263
|
+
--cpus=2 \
|
|
264
|
+
--pids-limit=200 \
|
|
265
|
+
clawspark-sandbox:latest \
|
|
266
|
+
python3 -c "print('sandbox_ok')" 2>&1 || echo "FAIL")
|
|
267
|
+
|
|
268
|
+
if [[ "${result}" == *"sandbox_ok"* ]]; then
|
|
269
|
+
printf '%sPASS%s\n' "${GREEN}" "${RESET}"
|
|
270
|
+
passed=$(( passed + 1 ))
|
|
271
|
+
else
|
|
272
|
+
printf '%sFAIL%s\n' "${RED}" "${RESET}"
|
|
273
|
+
failed=$(( failed + 1 ))
|
|
274
|
+
fi
|
|
275
|
+
|
|
276
|
+
# Test 2: Network isolation (should fail to reach the internet)
|
|
277
|
+
printf ' %s[2/5]%s Network isolation ... ' "${CYAN}" "${RESET}"
|
|
278
|
+
result=$(docker run --rm \
|
|
279
|
+
--read-only \
|
|
280
|
+
--tmpfs /tmp:size=100m \
|
|
281
|
+
--network=none \
|
|
282
|
+
--cap-drop=ALL \
|
|
283
|
+
--security-opt=no-new-privileges \
|
|
284
|
+
${seccomp_opt} \
|
|
285
|
+
--memory=1g \
|
|
286
|
+
--cpus=2 \
|
|
287
|
+
--pids-limit=200 \
|
|
288
|
+
clawspark-sandbox:latest \
|
|
289
|
+
python3 -c "
|
|
290
|
+
import urllib.request, sys
|
|
291
|
+
try:
|
|
292
|
+
urllib.request.urlopen('http://1.1.1.1', timeout=3)
|
|
293
|
+
print('NETWORK_OPEN')
|
|
294
|
+
except Exception:
|
|
295
|
+
print('NETWORK_BLOCKED')
|
|
296
|
+
" 2>&1 || echo "NETWORK_BLOCKED")
|
|
297
|
+
|
|
298
|
+
if [[ "${result}" == *"NETWORK_BLOCKED"* ]]; then
|
|
299
|
+
printf '%sPASS%s (no outbound access)\n' "${GREEN}" "${RESET}"
|
|
300
|
+
passed=$(( passed + 1 ))
|
|
301
|
+
else
|
|
302
|
+
printf '%sFAIL%s (network was reachable)\n' "${RED}" "${RESET}"
|
|
303
|
+
failed=$(( failed + 1 ))
|
|
304
|
+
fi
|
|
305
|
+
|
|
306
|
+
# Test 3: Read-only filesystem
|
|
307
|
+
printf ' %s[3/5]%s Read-only rootfs ... ' "${CYAN}" "${RESET}"
|
|
308
|
+
result=$(docker run --rm \
|
|
309
|
+
--read-only \
|
|
310
|
+
--tmpfs /tmp:size=100m \
|
|
311
|
+
--tmpfs /sandbox/work:size=500m \
|
|
312
|
+
--network=none \
|
|
313
|
+
--cap-drop=ALL \
|
|
314
|
+
--security-opt=no-new-privileges \
|
|
315
|
+
${seccomp_opt} \
|
|
316
|
+
--memory=1g \
|
|
317
|
+
--cpus=2 \
|
|
318
|
+
--pids-limit=200 \
|
|
319
|
+
clawspark-sandbox:latest \
|
|
320
|
+
python3 -c "
|
|
321
|
+
import os, sys
|
|
322
|
+
try:
|
|
323
|
+
with open('/etc/test_write', 'w') as f:
|
|
324
|
+
f.write('test')
|
|
325
|
+
print('WRITABLE')
|
|
326
|
+
except (OSError, PermissionError):
|
|
327
|
+
print('READONLY')
|
|
328
|
+
" 2>&1 || echo "READONLY")
|
|
329
|
+
|
|
330
|
+
if [[ "${result}" == *"READONLY"* ]]; then
|
|
331
|
+
printf '%sPASS%s (writes blocked)\n' "${GREEN}" "${RESET}"
|
|
332
|
+
passed=$(( passed + 1 ))
|
|
333
|
+
else
|
|
334
|
+
printf '%sFAIL%s (rootfs was writable)\n' "${RED}" "${RESET}"
|
|
335
|
+
failed=$(( failed + 1 ))
|
|
336
|
+
fi
|
|
337
|
+
|
|
338
|
+
# Test 4: tmpfs is writable (sandbox work area should work)
|
|
339
|
+
printf ' %s[4/5]%s Tmpfs work area ... ' "${CYAN}" "${RESET}"
|
|
340
|
+
result=$(docker run --rm \
|
|
341
|
+
--read-only \
|
|
342
|
+
--tmpfs /tmp:size=100m \
|
|
343
|
+
--tmpfs /sandbox/work:size=500m \
|
|
344
|
+
--network=none \
|
|
345
|
+
--cap-drop=ALL \
|
|
346
|
+
--security-opt=no-new-privileges \
|
|
347
|
+
${seccomp_opt} \
|
|
348
|
+
--memory=1g \
|
|
349
|
+
--cpus=2 \
|
|
350
|
+
--pids-limit=200 \
|
|
351
|
+
clawspark-sandbox:latest \
|
|
352
|
+
python3 -c "
|
|
353
|
+
with open('/sandbox/work/test.txt', 'w') as f:
|
|
354
|
+
f.write('hello')
|
|
355
|
+
with open('/sandbox/work/test.txt', 'r') as f:
|
|
356
|
+
data = f.read()
|
|
357
|
+
print('TMPFS_OK' if data == 'hello' else 'TMPFS_FAIL')
|
|
358
|
+
" 2>&1 || echo "TMPFS_FAIL")
|
|
359
|
+
|
|
360
|
+
if [[ "${result}" == *"TMPFS_OK"* ]]; then
|
|
361
|
+
printf '%sPASS%s (work area writable)\n' "${GREEN}" "${RESET}"
|
|
362
|
+
passed=$(( passed + 1 ))
|
|
363
|
+
else
|
|
364
|
+
printf '%sFAIL%s (work area not writable)\n' "${RED}" "${RESET}"
|
|
365
|
+
failed=$(( failed + 1 ))
|
|
366
|
+
fi
|
|
367
|
+
|
|
368
|
+
# Test 5: Non-root user
|
|
369
|
+
printf ' %s[5/5]%s Non-root user ... ' "${CYAN}" "${RESET}"
|
|
370
|
+
result=$(docker run --rm \
|
|
371
|
+
--read-only \
|
|
372
|
+
--tmpfs /tmp:size=100m \
|
|
373
|
+
--tmpfs /sandbox/work:size=500m \
|
|
374
|
+
--network=none \
|
|
375
|
+
--cap-drop=ALL \
|
|
376
|
+
--security-opt=no-new-privileges \
|
|
377
|
+
${seccomp_opt} \
|
|
378
|
+
--memory=1g \
|
|
379
|
+
--cpus=2 \
|
|
380
|
+
--pids-limit=200 \
|
|
381
|
+
clawspark-sandbox:latest \
|
|
382
|
+
python3 -c "
|
|
383
|
+
import os
|
|
384
|
+
uid = os.getuid()
|
|
385
|
+
print('NONROOT' if uid != 0 else 'ROOT')
|
|
386
|
+
" 2>&1 || echo "ROOT")
|
|
387
|
+
|
|
388
|
+
if [[ "${result}" == *"NONROOT"* ]]; then
|
|
389
|
+
printf '%sPASS%s (running as non-root)\n' "${GREEN}" "${RESET}"
|
|
390
|
+
passed=$(( passed + 1 ))
|
|
391
|
+
else
|
|
392
|
+
printf '%sFAIL%s (running as root)\n' "${RED}" "${RESET}"
|
|
393
|
+
failed=$(( failed + 1 ))
|
|
394
|
+
fi
|
|
395
|
+
|
|
396
|
+
# Summary
|
|
397
|
+
printf '\n'
|
|
398
|
+
if (( failed == 0 )); then
|
|
399
|
+
print_box \
|
|
400
|
+
"${GREEN}${BOLD}All ${passed} tests passed${RESET}" \
|
|
401
|
+
"" \
|
|
402
|
+
"The sandbox is working correctly." \
|
|
403
|
+
"Agent-generated code will execute in a hardened container."
|
|
404
|
+
else
|
|
405
|
+
print_box \
|
|
406
|
+
"${RED}${BOLD}${failed} of $(( passed + failed )) tests failed${RESET}" \
|
|
407
|
+
"" \
|
|
408
|
+
"Some sandbox security checks did not pass." \
|
|
409
|
+
"Review Docker configuration and try again."
|
|
410
|
+
fi
|
|
411
|
+
printf '\n'
|
|
412
|
+
|
|
413
|
+
# Return non-zero if any test failed
|
|
414
|
+
(( failed > 0 )) && return 1 || return 0
|
|
415
|
+
}
|
package/lib/secure.sh
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# lib/secure.sh — Security hardening: token generation, firewall rules,
|
|
3
|
+
# air-gap mode, and file permissions.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
secure_setup() {
|
|
7
|
+
log_info "Applying security hardening..."
|
|
8
|
+
hr
|
|
9
|
+
|
|
10
|
+
# ── Access token ────────────────────────────────────────────────────────
|
|
11
|
+
local token_file="${CLAWSPARK_DIR}/token"
|
|
12
|
+
if [[ ! -f "${token_file}" ]]; then
|
|
13
|
+
local token
|
|
14
|
+
token=$(openssl rand -hex 32 2>/dev/null || head -c 64 /dev/urandom | od -An -tx1 | tr -d ' \n')
|
|
15
|
+
echo "${token}" > "${token_file}"
|
|
16
|
+
chmod 600 "${token_file}"
|
|
17
|
+
log_success "Access token generated and saved to ${token_file}"
|
|
18
|
+
else
|
|
19
|
+
log_info "Access token already exists."
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
# ── Gateway binds to localhost by default ────────────────────────────────
|
|
23
|
+
# OpenClaw v2026+ gateway.mode=local already binds to loopback.
|
|
24
|
+
# Nothing to inject; config was set during setup_openclaw.
|
|
25
|
+
log_success "Gateway bound to localhost (gateway.mode=local)."
|
|
26
|
+
|
|
27
|
+
# ── File permissions ────────────────────────────────────────────────────
|
|
28
|
+
chmod 700 "${HOME}/.openclaw" 2>/dev/null || true
|
|
29
|
+
chmod 700 "${CLAWSPARK_DIR}" 2>/dev/null || true
|
|
30
|
+
log_success "Restricted permissions on ~/.openclaw and ~/.clawspark"
|
|
31
|
+
|
|
32
|
+
# ── Firewall (UFW) ─────────────────────────────────────────────────────
|
|
33
|
+
if check_command ufw && sudo -n true 2>/dev/null; then
|
|
34
|
+
_configure_ufw
|
|
35
|
+
elif check_command ufw; then
|
|
36
|
+
log_info "UFW found but sudo not available -- skipping firewall config."
|
|
37
|
+
log_info "Run 'sudo ufw enable' manually to configure firewall."
|
|
38
|
+
else
|
|
39
|
+
log_info "UFW not found — skipping firewall configuration."
|
|
40
|
+
log_info "Consider installing a firewall for production use."
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# ── Air-gap mode ────────────────────────────────────────────────────────
|
|
44
|
+
if [[ "${AIR_GAP:-false}" == "true" ]]; then
|
|
45
|
+
_configure_airgap
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# ── Code-level tool restrictions ─────────────────────────────────────────
|
|
49
|
+
# These are enforced by OpenClaw runtime, NOT by prompt instructions.
|
|
50
|
+
# Even if the agent is prompt-injected, it cannot bypass these.
|
|
51
|
+
_harden_tool_access
|
|
52
|
+
|
|
53
|
+
# ── Security warnings ───────────────────────────────────────────────────
|
|
54
|
+
printf '\n'
|
|
55
|
+
print_box \
|
|
56
|
+
"${YELLOW}${BOLD}Security Notes${RESET}" \
|
|
57
|
+
"" \
|
|
58
|
+
"1. Local models can still be susceptible to prompt injection." \
|
|
59
|
+
" Do not expose the API to untrusted users." \
|
|
60
|
+
"2. The gateway binds to localhost only by default." \
|
|
61
|
+
"3. Your access token is in ~/.clawspark/token" \
|
|
62
|
+
"4. Review firewall rules with: sudo ufw status verbose" \
|
|
63
|
+
"5. File operations restricted to workspace (tools.fs.workspaceOnly)" \
|
|
64
|
+
"6. Dangerous commands blocked via gateway.nodes.denyCommands" \
|
|
65
|
+
"7. Plugin approval hooks enabled (plugins.requireApproval)"
|
|
66
|
+
|
|
67
|
+
log_success "Security hardening complete."
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# ── Code-level tool & filesystem restrictions ─────────────────────────────
|
|
71
|
+
# These are enforced by OpenClaw's runtime, not by SOUL.md/TOOLS.md prompts.
|
|
72
|
+
# A prompt injection CANNOT bypass these restrictions.
|
|
73
|
+
|
|
74
|
+
_harden_tool_access() {
|
|
75
|
+
log_info "Applying code-level tool restrictions..."
|
|
76
|
+
local config_file="${HOME}/.openclaw/openclaw.json"
|
|
77
|
+
|
|
78
|
+
if [[ ! -f "${config_file}" ]]; then
|
|
79
|
+
log_warn "Config not found -- skipping tool hardening."
|
|
80
|
+
return 0
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
python3 -c "
|
|
84
|
+
import json, sys
|
|
85
|
+
|
|
86
|
+
path = sys.argv[1]
|
|
87
|
+
with open(path) as f:
|
|
88
|
+
cfg = json.load(f)
|
|
89
|
+
|
|
90
|
+
# ── 1. Restrict filesystem to workspace only ──────────────────────────
|
|
91
|
+
# This prevents the agent from reading files outside ~/workspace
|
|
92
|
+
# even if prompted to. The read/write/edit tools are code-gated.
|
|
93
|
+
cfg.setdefault('tools', {})
|
|
94
|
+
cfg['tools'].setdefault('fs', {})
|
|
95
|
+
cfg['tools']['fs']['workspaceOnly'] = True
|
|
96
|
+
|
|
97
|
+
# ── 2. Enable plugin approval hooks (v2026.3.22+) ────────────────────
|
|
98
|
+
# Plugins can pause tool execution and prompt for user confirmation
|
|
99
|
+
# via messaging channels. This prevents rogue plugins from acting
|
|
100
|
+
# without explicit approval.
|
|
101
|
+
cfg.setdefault('plugins', {})
|
|
102
|
+
cfg['plugins']['requireApproval'] = True
|
|
103
|
+
|
|
104
|
+
# ── 3. Block dangerous exec commands at the gateway level ─────────────
|
|
105
|
+
# These are command prefixes that the node host will REFUSE to execute,
|
|
106
|
+
# regardless of what the agent requests. Code-enforced deny list.
|
|
107
|
+
cfg.setdefault('gateway', {})
|
|
108
|
+
cfg['gateway'].setdefault('nodes', {})
|
|
109
|
+
cfg['gateway']['nodes']['denyCommands'] = [
|
|
110
|
+
# Credential/secret exfiltration
|
|
111
|
+
'cat ~/.openclaw',
|
|
112
|
+
'cat /etc/shadow',
|
|
113
|
+
'cat ~/.ssh',
|
|
114
|
+
# Destructive operations
|
|
115
|
+
'rm -rf /',
|
|
116
|
+
'rm -rf ~',
|
|
117
|
+
'mkfs',
|
|
118
|
+
'dd if=',
|
|
119
|
+
# Network exfiltration of secrets
|
|
120
|
+
'curl.*gateway.env',
|
|
121
|
+
'curl.*gateway-token',
|
|
122
|
+
'wget.*gateway.env',
|
|
123
|
+
# System modification
|
|
124
|
+
'passwd',
|
|
125
|
+
'useradd',
|
|
126
|
+
'usermod',
|
|
127
|
+
'visudo',
|
|
128
|
+
'crontab',
|
|
129
|
+
# Package management (prevent installing malware)
|
|
130
|
+
'apt install',
|
|
131
|
+
'apt-get install',
|
|
132
|
+
'pip install',
|
|
133
|
+
'npm install -g',
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
with open(path, 'w') as f:
|
|
137
|
+
json.dump(cfg, f, indent=2)
|
|
138
|
+
print('ok')
|
|
139
|
+
" "${config_file}" 2>> "${CLAWSPARK_LOG}" || {
|
|
140
|
+
log_warn "Tool hardening failed. Continuing with default permissions."
|
|
141
|
+
return 0
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
log_success "Code-level restrictions applied:"
|
|
145
|
+
log_info " tools.fs.workspaceOnly = true (file ops restricted to workspace)"
|
|
146
|
+
log_info " plugins.requireApproval = true (plugins need user confirmation)"
|
|
147
|
+
log_info " gateway.nodes.denyCommands = [21 blocked patterns]"
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# ── UFW configuration ──────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
_configure_ufw() {
|
|
153
|
+
log_info "Configuring UFW firewall rules..."
|
|
154
|
+
|
|
155
|
+
sudo ufw default deny incoming >> "${CLAWSPARK_LOG}" 2>&1 || true
|
|
156
|
+
sudo ufw default allow outgoing >> "${CLAWSPARK_LOG}" 2>&1 || true
|
|
157
|
+
sudo ufw allow ssh >> "${CLAWSPARK_LOG}" 2>&1 || true
|
|
158
|
+
|
|
159
|
+
# Enable UFW if not already active (non-interactive)
|
|
160
|
+
if ! sudo ufw status | grep -q "Status: active"; then
|
|
161
|
+
echo "y" | sudo ufw enable >> "${CLAWSPARK_LOG}" 2>&1 || {
|
|
162
|
+
log_warn "Could not enable UFW automatically."
|
|
163
|
+
}
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
log_success "UFW configured: deny incoming, allow outgoing, allow SSH."
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# ── Air-gap mode ────────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
_configure_airgap() {
|
|
172
|
+
log_info "Configuring air-gap mode..."
|
|
173
|
+
|
|
174
|
+
printf '\n'
|
|
175
|
+
print_box \
|
|
176
|
+
"${RED}${BOLD}AIR-GAP MODE${RESET}" \
|
|
177
|
+
"" \
|
|
178
|
+
"After setup completes, outgoing internet will be blocked." \
|
|
179
|
+
"Only local network traffic will be allowed." \
|
|
180
|
+
"Use 'clawspark airgap off' to restore connectivity."
|
|
181
|
+
|
|
182
|
+
if check_command ufw; then
|
|
183
|
+
# Block outgoing by default
|
|
184
|
+
sudo ufw default deny outgoing >> "${CLAWSPARK_LOG}" 2>&1 || true
|
|
185
|
+
# Allow local network
|
|
186
|
+
sudo ufw allow out to 10.0.0.0/8 >> "${CLAWSPARK_LOG}" 2>&1 || true
|
|
187
|
+
sudo ufw allow out to 172.16.0.0/12 >> "${CLAWSPARK_LOG}" 2>&1 || true
|
|
188
|
+
sudo ufw allow out to 192.168.0.0/16 >> "${CLAWSPARK_LOG}" 2>&1 || true
|
|
189
|
+
# Allow loopback
|
|
190
|
+
sudo ufw allow out to 127.0.0.0/8 >> "${CLAWSPARK_LOG}" 2>&1 || true
|
|
191
|
+
# Allow DNS (needed for local resolution)
|
|
192
|
+
sudo ufw allow out 53 >> "${CLAWSPARK_LOG}" 2>&1 || true
|
|
193
|
+
log_success "UFW air-gap rules applied."
|
|
194
|
+
else
|
|
195
|
+
log_warn "UFW not available — cannot enforce air-gap at firewall level."
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
# Create toggle script
|
|
199
|
+
_create_airgap_toggle
|
|
200
|
+
|
|
201
|
+
echo "true" > "${CLAWSPARK_DIR}/airgap.state"
|
|
202
|
+
log_success "Air-gap mode enabled."
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
_create_airgap_toggle() {
|
|
206
|
+
local toggle_script="${CLAWSPARK_DIR}/airgap-toggle.sh"
|
|
207
|
+
cat > "${toggle_script}" <<'TOGGLE_EOF'
|
|
208
|
+
#!/usr/bin/env bash
|
|
209
|
+
# airgap-toggle.sh — Enable or disable air-gap mode.
|
|
210
|
+
set -euo pipefail
|
|
211
|
+
|
|
212
|
+
STATE_FILE="${HOME}/.clawspark/airgap.state"
|
|
213
|
+
|
|
214
|
+
usage() {
|
|
215
|
+
echo "Usage: $0 [on|off]"
|
|
216
|
+
exit 1
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
[[ $# -ne 1 ]] && usage
|
|
220
|
+
|
|
221
|
+
case "$1" in
|
|
222
|
+
on)
|
|
223
|
+
sudo ufw default deny outgoing
|
|
224
|
+
sudo ufw allow out to 10.0.0.0/8
|
|
225
|
+
sudo ufw allow out to 172.16.0.0/12
|
|
226
|
+
sudo ufw allow out to 192.168.0.0/16
|
|
227
|
+
sudo ufw allow out to 127.0.0.0/8
|
|
228
|
+
sudo ufw allow out 53
|
|
229
|
+
echo "true" > "${STATE_FILE}"
|
|
230
|
+
echo "Air-gap mode ENABLED. Outgoing internet blocked."
|
|
231
|
+
;;
|
|
232
|
+
off)
|
|
233
|
+
sudo ufw default allow outgoing
|
|
234
|
+
echo "false" > "${STATE_FILE}"
|
|
235
|
+
echo "Air-gap mode DISABLED. Outgoing internet restored."
|
|
236
|
+
;;
|
|
237
|
+
*)
|
|
238
|
+
usage
|
|
239
|
+
;;
|
|
240
|
+
esac
|
|
241
|
+
TOGGLE_EOF
|
|
242
|
+
chmod +x "${toggle_script}"
|
|
243
|
+
log_info "Air-gap toggle script: ${toggle_script}"
|
|
244
|
+
}
|