@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.
Files changed (49) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/LICENSE +21 -0
  3. package/README.md +378 -0
  4. package/clawspark +2715 -0
  5. package/configs/models.yaml +108 -0
  6. package/configs/skill-packs.yaml +44 -0
  7. package/configs/skills.yaml +37 -0
  8. package/install.sh +387 -0
  9. package/lib/common.sh +249 -0
  10. package/lib/detect-hardware.sh +156 -0
  11. package/lib/diagnose.sh +636 -0
  12. package/lib/render-diagram.sh +47 -0
  13. package/lib/sandbox-commands.sh +415 -0
  14. package/lib/secure.sh +244 -0
  15. package/lib/select-model.sh +442 -0
  16. package/lib/setup-browser.sh +138 -0
  17. package/lib/setup-dashboard.sh +228 -0
  18. package/lib/setup-inference.sh +128 -0
  19. package/lib/setup-mcp.sh +142 -0
  20. package/lib/setup-messaging.sh +242 -0
  21. package/lib/setup-models.sh +121 -0
  22. package/lib/setup-openclaw.sh +808 -0
  23. package/lib/setup-sandbox.sh +188 -0
  24. package/lib/setup-skills.sh +113 -0
  25. package/lib/setup-systemd.sh +224 -0
  26. package/lib/setup-tailscale.sh +188 -0
  27. package/lib/setup-voice.sh +101 -0
  28. package/lib/skill-audit.sh +449 -0
  29. package/lib/verify.sh +177 -0
  30. package/package.json +57 -0
  31. package/scripts/release.sh +133 -0
  32. package/uninstall.sh +161 -0
  33. package/v2/README.md +50 -0
  34. package/v2/configs/providers.yaml +79 -0
  35. package/v2/configs/skills.yaml +36 -0
  36. package/v2/install.sh +116 -0
  37. package/v2/lib/common.sh +285 -0
  38. package/v2/lib/detect-hardware.sh +119 -0
  39. package/v2/lib/select-runtime.sh +273 -0
  40. package/v2/lib/setup-extras.sh +95 -0
  41. package/v2/lib/setup-openclaw.sh +187 -0
  42. package/v2/lib/setup-provider.sh +131 -0
  43. package/v2/lib/verify.sh +133 -0
  44. package/web/index.html +1835 -0
  45. package/web/install.sh +387 -0
  46. package/web/logo-hero.svg +11 -0
  47. package/web/logo-icon.svg +12 -0
  48. package/web/logo.svg +17 -0
  49. 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
+ }