anurag008w commited on
Commit
c26fdc3
Β·
1 Parent(s): 49cc5fe

Allow startup installed plugins before gateway

Browse files
Files changed (3) hide show
  1. Dockerfile +11 -2
  2. README.md +45 -0
  3. start.sh +420 -14
Dockerfile CHANGED
@@ -14,6 +14,7 @@ ARG OPENCLAW_VERSION=latest
14
  # Install system dependencies
15
  RUN apt-get update && apt-get install -y \
16
  git \
 
17
  ca-certificates \
18
  jq \
19
  curl \
@@ -44,9 +45,17 @@ RUN apt-get update && apt-get install -y \
44
  pip3 install --no-cache-dir --break-system-packages huggingface_hub && \
45
  rm -rf /var/lib/apt/lists/*
46
 
47
- # Reuse existing node user (UID 1000)
 
 
48
  RUN mkdir -p /home/node/app /home/node/.openclaw && \
49
- chown -R 1000:1000 /home/node
 
 
 
 
 
 
50
 
51
  # Copy pre-built OpenClaw (skips npm install entirely β€” much faster!)
52
  COPY --from=openclaw --chown=1000:1000 /app /home/node/.openclaw/openclaw-app
 
14
  # Install system dependencies
15
  RUN apt-get update && apt-get install -y \
16
  git \
17
+ sudo \
18
  ca-certificates \
19
  jq \
20
  curl \
 
45
  pip3 install --no-cache-dir --break-system-packages huggingface_hub && \
46
  rm -rf /var/lib/apt/lists/*
47
 
48
+ # Reuse existing node user (UID 1000). Allow passwordless package-manager
49
+ # commands only so runtime apt installs can be replayed after HF Space restarts
50
+ # without granting unrestricted sudo access.
51
  RUN mkdir -p /home/node/app /home/node/.openclaw && \
52
+ chown -R 1000:1000 /home/node && \
53
+ printf '%s\n' \
54
+ 'Cmnd_Alias HUGGINGCLAW_APT = /usr/bin/apt, /usr/bin/apt-get, /usr/bin/dpkg' \
55
+ 'node ALL=(root) NOPASSWD: HUGGINGCLAW_APT' \
56
+ > /etc/sudoers.d/huggingclaw-apt && \
57
+ chmod 0440 /etc/sudoers.d/huggingclaw-apt && \
58
+ visudo -cf /etc/sudoers.d/huggingclaw-apt
59
 
60
  # Copy pre-built OpenClaw (skips npm install entirely β€” much faster!)
61
  COPY --from=openclaw --chown=1000:1000 /app /home/node/.openclaw/openclaw-app
README.md CHANGED
@@ -164,6 +164,51 @@ HuggingClaw automatically syncs your workspace (chats, settings, sessions) to a
164
  | `HF_TOKEN` | β€” | HF token with **Write** access |
165
  | `SYNC_INTERVAL` | `180` | Backup frequency in seconds |
166
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  ## πŸ’“ Staying Alive *(Recommended on Free HF Spaces)*
168
 
169
  Your Space will automatically be kept awake by a background Cloudflare Worker when you configure the `CLOUDFLARE_WORKERS_TOKEN` secret. The worker uses a cron trigger to regularly ping your Space's `/health` endpoint. The dashboard displays the current keep-alive worker status.
 
164
  | `HF_TOKEN` | β€” | HF token with **Write** access |
165
  | `SYNC_INTERVAL` | `180` | Backup frequency in seconds |
166
 
167
+ ## πŸ“¦ Ephemeral Package Re-install *(Optional)*
168
+
169
+ Yes β€” you can use extra packages after a Space restart without storing package files. The easiest option is to remember **one variable**:
170
+
171
+ | Variable | What to put in it |
172
+ | :--- | :--- |
173
+ | `HUGGINGCLAW_RUN` | Any bash commands you want to run on every startup |
174
+
175
+ Example:
176
+
177
+ ```bash
178
+ HUGGINGCLAW_RUN="""
179
+ set -e
180
+ sudo apt-get update
181
+ sudo apt-get install -y ffmpeg
182
+ python3 -m pip install --user pandas requests
183
+ npm install -g typescript
184
+ """
185
+ ```
186
+
187
+ For very quote-heavy or strange scripts, put a base64 script in the same variable:
188
+
189
+ ```bash
190
+ # locally
191
+ base64 -w0 setup.sh
192
+
193
+ # HF Variable
194
+ HUGGINGCLAW_RUN=base64:<paste-output-here>
195
+ ```
196
+
197
+ How it works:
198
+
199
+ 1. `HUGGINGCLAW_RUN` is run as a full bash script on every boot before the OpenClaw gateway launches, so multi-line commands, `if`, loops, functions, and heredocs work. Long installs will delay gateway startup.
200
+ 2. Startup scripts load the same HuggingClaw shell wrappers as the interactive shell, so `apt install ...`, `pip install ...`, `npm install -g ...`, and `openclaw plugins install ...` behave consistently.
201
+ 3. OpenClaw plugins installed at startup are synced into `plugins.allow` before the gateway launches, so the gateway can load them instead of reporting them as not installed.
202
+ 4. If you install from the OpenClaw shell manually, HuggingClaw records only successful install commands in `/home/node/.openclaw/workspace/startup.sh` for replay. Failed or dummy commands are not saved by the wrapper.
203
+ 5. Package files are not persisted; commands are replayed to reconstruct them after restart.
204
+
205
+ Errors are always printed as `ERROR:` lines in Space logs. By default HuggingClaw logs the error and continues booting; set `HUGGINGCLAW_STARTUP_STRICT=true` if the Space should fail fast when any startup install command fails.
206
+
207
+ Advanced/backward-compatible variables still work if you prefer package-specific fields: `HUGGINGCLAW_APT_PACKAGES`, `HUGGINGCLAW_PIP_PACKAGES`, `HUGGINGCLAW_NPM_PACKAGES`, `HUGGINGCLAW_OPENCLAW_PLUGINS`, `HUGGINGCLAW_STARTUP_COMMANDS`, `HUGGINGCLAW_STARTUP_COMMAND_1`...`100`, `HUGGINGCLAW_STARTUP_SCRIPT`, and `HUGGINGCLAW_STARTUP_SCRIPT_B64`.
208
+
209
+ > [!IMPORTANT]
210
+ > `sudo` is available for package-manager commands only (`apt`, `apt-get`, and `dpkg`). This is enough for `sudo apt-get update` and `sudo apt-get install -y ...`, but it is not unrestricted root access. Apt-installed packages still disappear on Space restart, so put them in `HUGGINGCLAW_RUN` or let the shell wrapper record the command in `startup.sh`.
211
+
212
  ## πŸ’“ Staying Alive *(Recommended on Free HF Spaces)*
213
 
214
  Your Space will automatically be kept awake by a background Cloudflare Worker when you configure the `CLOUDFLARE_WORKERS_TOKEN` secret. The worker uses a cron trigger to regularly ping your Space's `/health` endpoint. The dashboard displays the current keep-alive worker status.
start.sh CHANGED
@@ -129,9 +129,20 @@ mkdir -p /home/node/.openclaw/credentials
129
  mkdir -p /home/node/.openclaw/memory
130
  mkdir -p /home/node/.openclaw/extensions
131
  mkdir -p /home/node/.openclaw/workspace
 
132
  chmod 700 /home/node/.openclaw
133
  chmod 700 /home/node/.openclaw/credentials
134
 
 
 
 
 
 
 
 
 
 
 
135
  # ── Restore workspace/state from HF Dataset ──
136
  BACKUP_DATASET="${BACKUP_DATASET_NAME:-huggingclaw-backup}"
137
  if [ -n "${HF_TOKEN:-}" ]; then
@@ -421,19 +432,36 @@ fi
421
 
422
  # Write config
423
  EXISTING_CONFIG="/home/node/.openclaw/openclaw.json"
 
 
 
 
424
  if [ -f "$EXISTING_CONFIG" ]; then
425
- echo "Restored config found β€” patching required fields only..."
426
  PATCHED=$(jq \
427
  --arg token "$GATEWAY_TOKEN" \
428
  --arg model "$LLM_MODEL" \
429
  --arg fileLevel "$OPENCLAW_FILE_LOG_LEVEL" \
430
  --arg consoleLevel "$OPENCLAW_CONSOLE_LOG_LEVEL" \
431
  --arg consoleStyle "$OPENCLAW_CONSOLE_LOG_STYLE" \
 
 
432
  '.gateway.auth.token = $token
433
  | .agents.defaults.model = $model
434
  | .logging.level = $fileLevel
435
  | .logging.consoleLevel = $consoleLevel
436
- | .logging.consoleStyle = $consoleStyle' \
 
 
 
 
 
 
 
 
 
 
 
437
  "$EXISTING_CONFIG" 2>/dev/null)
438
 
439
  if [ -n "$PATCHED" ]; then
@@ -537,26 +565,190 @@ if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ]; then
537
  fi
538
 
539
  # ── Write shell capture wrappers to .bashrc ──
540
- STARTUP_FILE="/home/node/.openclaw/workspace/startup.sh"
 
 
 
 
 
 
541
  cat > /home/node/.bashrc << 'BASHRC'
 
 
 
 
 
542
  STARTUP_FILE="/home/node/.openclaw/workspace/startup.sh"
543
  _hc_append() {
544
  local line="$*"
 
 
 
545
  grep -qxF "$line" "$STARTUP_FILE" 2>/dev/null || echo "$line" >> "$STARTUP_FILE"
546
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
547
  apt-get() {
548
- command apt-get "$@"
549
- [[ "$1" == "install" ]] && _hc_append "apt-get install -y ${@:2}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
550
  }
551
  apt() {
552
- command apt "$@"
553
- [[ "$1" == "install" ]] && _hc_append "apt-get install -y ${@:2}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
554
  }
555
- pip() { command pip "$@"; [[ "$1" == "install" ]] && _hc_append "pip install ${@:2}"; }
556
- pip3() { command pip3 "$@"; [[ "$1" == "install" ]] && _hc_append "pip3 install ${@:2}"; }
557
- npm() { command npm "$@"; [[ "$1" == "install" && "$2" == "-g" ]] && _hc_append "npm install -g ${@:3}"; }
558
- openclaw() { command openclaw "$@"; [[ "$1" == "plugins" && "$2" == "install" ]] && _hc_append "openclaw plugins install ${@:3}"; }
559
  BASHRC
 
 
 
560
  echo "Shell capture wrappers ready."
561
 
562
  # ── Re-install previously installed plugins ──
@@ -580,6 +772,218 @@ if [ -f "$EXISTING_CONFIG" ]; then
580
  fi
581
  fi
582
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
583
  # ── Run workspace startup script ──
584
  STARTUP_FILE="/home/node/.openclaw/workspace/startup.sh"
585
  if [ ! -f "$STARTUP_FILE" ]; then
@@ -588,10 +992,12 @@ if [ ! -f "$STARTUP_FILE" ]; then
588
  echo "Created workspace/startup.sh"
589
  fi
590
  if [ -s "$STARTUP_FILE" ]; then
591
- echo "Running workspace/startup.sh..."
592
- bash "$STARTUP_FILE" || echo "Warning: startup.sh had errors, continuing..."
593
- echo "Startup script complete."
594
  fi
 
 
595
 
596
  # ── Launch gateway ──
597
  echo "Launching OpenClaw gateway on port 7860..."
 
129
  mkdir -p /home/node/.openclaw/memory
130
  mkdir -p /home/node/.openclaw/extensions
131
  mkdir -p /home/node/.openclaw/workspace
132
+ mkdir -p /home/node/.local/bin /home/node/.local/lib /home/node/.npm-global
133
  chmod 700 /home/node/.openclaw
134
  chmod 700 /home/node/.openclaw/credentials
135
 
136
+ # User-installed packages are intentionally ephemeral in the container. Keep
137
+ # npm/pip installs in user-writable locations, make apt noninteractive,
138
+ # and persist only a tiny replay script in the synced workspace so packages
139
+ # are re-installed after restart.
140
+ export NPM_CONFIG_PREFIX="${NPM_CONFIG_PREFIX:-/home/node/.local}"
141
+ export npm_config_prefix="$NPM_CONFIG_PREFIX"
142
+ export PYTHONUSERBASE="${PYTHONUSERBASE:-/home/node/.local}"
143
+ export DEBIAN_FRONTEND="${DEBIAN_FRONTEND:-noninteractive}"
144
+ STARTUP_FILE="/home/node/.openclaw/workspace/startup.sh"
145
+
146
  # ── Restore workspace/state from HF Dataset ──
147
  BACKUP_DATASET="${BACKUP_DATASET_NAME:-huggingclaw-backup}"
148
  if [ -n "${HF_TOKEN:-}" ]; then
 
432
 
433
  # Write config
434
  EXISTING_CONFIG="/home/node/.openclaw/openclaw.json"
435
+ WHATSAPP_CONFIG_ENABLED=false
436
+ if [ "$WHATSAPP_ENABLED_NORMALIZED" = "true" ]; then
437
+ WHATSAPP_CONFIG_ENABLED=true
438
+ fi
439
  if [ -f "$EXISTING_CONFIG" ]; then
440
+ echo "Restored config found β€” patching required fields and runtime channel/plugin toggles..."
441
  PATCHED=$(jq \
442
  --arg token "$GATEWAY_TOKEN" \
443
  --arg model "$LLM_MODEL" \
444
  --arg fileLevel "$OPENCLAW_FILE_LOG_LEVEL" \
445
  --arg consoleLevel "$OPENCLAW_CONSOLE_LOG_LEVEL" \
446
  --arg consoleStyle "$OPENCLAW_CONSOLE_LOG_STYLE" \
447
+ --argjson desired "$CONFIG_JSON" \
448
+ --argjson whatsappEnabled "$WHATSAPP_CONFIG_ENABLED" \
449
  '.gateway.auth.token = $token
450
  | .agents.defaults.model = $model
451
  | .logging.level = $fileLevel
452
  | .logging.consoleLevel = $consoleLevel
453
+ | .logging.consoleStyle = $consoleStyle
454
+ | .channels = ((.channels // {}) * ($desired.channels // {}))
455
+ | .plugins.allow = (((.plugins.allow // []) + ($desired.plugins.allow // [])) | unique)
456
+ | .plugins.deny = (((.plugins.deny // []) + ($desired.plugins.deny // [])) | unique)
457
+ | .plugins.entries = ((.plugins.entries // {}) * ($desired.plugins.entries // {}))
458
+ | if $whatsappEnabled then
459
+ .plugins.entries.whatsapp.enabled = true
460
+ | .channels.whatsapp = ($desired.channels.whatsapp // {"dmPolicy": "pairing"})
461
+ else
462
+ .plugins.entries.whatsapp.enabled = false
463
+ | del(.channels.whatsapp)
464
+ end' \
465
  "$EXISTING_CONFIG" 2>/dev/null)
466
 
467
  if [ -n "$PATCHED" ]; then
 
565
  fi
566
 
567
  # ── Write shell capture wrappers to .bashrc ──
568
+ # The wrappers persist only install commands, not downloaded package files.
569
+ # On the next boot the synced workspace/startup.sh replays those commands.
570
+ if [ ! -f "$STARTUP_FILE" ]; then
571
+ touch "$STARTUP_FILE"
572
+ chmod +x "$STARTUP_FILE"
573
+ echo "Created workspace/startup.sh"
574
+ fi
575
  cat > /home/node/.bashrc << 'BASHRC'
576
+ export PATH="/home/node/.local/bin:$PATH"
577
+ export NPM_CONFIG_PREFIX="${NPM_CONFIG_PREFIX:-/home/node/.local}"
578
+ export npm_config_prefix="$NPM_CONFIG_PREFIX"
579
+ export PYTHONUSERBASE="${PYTHONUSERBASE:-/home/node/.local}"
580
+ export DEBIAN_FRONTEND="${DEBIAN_FRONTEND:-noninteractive}"
581
  STARTUP_FILE="/home/node/.openclaw/workspace/startup.sh"
582
  _hc_append() {
583
  local line="$*"
584
+ mkdir -p "$(dirname "$STARTUP_FILE")"
585
+ touch "$STARTUP_FILE"
586
+ chmod +x "$STARTUP_FILE" 2>/dev/null || true
587
  grep -qxF "$line" "$STARTUP_FILE" 2>/dev/null || echo "$line" >> "$STARTUP_FILE"
588
  }
589
+ _hc_quote_args() {
590
+ local quoted=()
591
+ local arg
592
+ for arg in "$@"; do
593
+ printf -v arg '%q' "$arg"
594
+ quoted+=("$arg")
595
+ done
596
+ printf '%s' "${quoted[*]}"
597
+ }
598
+ _hc_append_cmd() {
599
+ local cmd="$1"
600
+ shift
601
+ local args
602
+ args=$(_hc_quote_args "$@")
603
+ if [ -n "$args" ]; then
604
+ _hc_append "$cmd $args"
605
+ else
606
+ _hc_append "$cmd"
607
+ fi
608
+ }
609
+ _hc_allow_openclaw_plugins() {
610
+ local config="/home/node/.openclaw/openclaw.json"
611
+ [ -f "$config" ] || return 0
612
+
613
+ local plugins=()
614
+ local plugin
615
+ for plugin in "$@"; do
616
+ [ -n "$plugin" ] || continue
617
+ [[ "$plugin" == -* ]] && continue
618
+ plugins+=("$plugin")
619
+ if [[ "$plugin" == @openclaw/* ]]; then
620
+ plugins+=("${plugin#@openclaw/}")
621
+ fi
622
+ done
623
+ [ "${#plugins[@]}" -gt 0 ] || return 0
624
+
625
+ local plugins_json
626
+ plugins_json=$(printf '%s\n' "${plugins[@]}" | jq -R 'select(length > 0)' | jq -s 'unique') || return 0
627
+ jq --argjson plugins "$plugins_json" \
628
+ '.plugins.allow = (((.plugins.allow // []) + $plugins) | unique)' \
629
+ "$config" > "$config.tmp" && mv "$config.tmp" "$config"
630
+ }
631
+ _hc_has_arg() {
632
+ local needle="$1"
633
+ shift
634
+ local arg
635
+ for arg in "$@"; do
636
+ [ "$arg" = "$needle" ] && return 0
637
+ done
638
+ return 1
639
+ }
640
+ _hc_can_sudo_apt() {
641
+ command -v sudo >/dev/null 2>&1 && sudo -n apt-get --version >/dev/null 2>&1
642
+ }
643
+ _hc_apt_install() {
644
+ if [ "$(id -u)" -eq 0 ]; then
645
+ command apt-get update && command apt-get install -y "$@"
646
+ elif _hc_can_sudo_apt; then
647
+ sudo apt-get update && sudo apt-get install -y "$@"
648
+ else
649
+ echo "Error: apt install needs root. Rebuild with the latest HuggingClaw image or add packages to Dockerfile." >&2
650
+ return 1
651
+ fi
652
+ }
653
  apt-get() {
654
+ case "${1:-}" in
655
+ install)
656
+ shift
657
+ _hc_apt_install "$@"
658
+ local rc=$?
659
+ if [ $rc -eq 0 ]; then
660
+ _hc_append_cmd "sudo apt-get update && sudo apt-get install -y" "$@"
661
+ fi
662
+ return $rc
663
+ ;;
664
+ update)
665
+ if [ "$(id -u)" -eq 0 ]; then
666
+ command apt-get "$@"
667
+ elif _hc_can_sudo_apt; then
668
+ sudo apt-get "$@"
669
+ else
670
+ command apt-get "$@"
671
+ fi
672
+ return $?
673
+ ;;
674
+ *)
675
+ command apt-get "$@"
676
+ return $?
677
+ ;;
678
+ esac
679
  }
680
  apt() {
681
+ case "${1:-}" in
682
+ install)
683
+ shift
684
+ _hc_apt_install "$@"
685
+ local rc=$?
686
+ if [ $rc -eq 0 ]; then
687
+ _hc_append_cmd "sudo apt-get update && sudo apt-get install -y" "$@"
688
+ fi
689
+ return $rc
690
+ ;;
691
+ update)
692
+ if [ "$(id -u)" -eq 0 ]; then
693
+ command apt "$@"
694
+ elif _hc_can_sudo_apt; then
695
+ sudo apt "$@"
696
+ else
697
+ command apt "$@"
698
+ fi
699
+ return $?
700
+ ;;
701
+ *)
702
+ command apt "$@"
703
+ return $?
704
+ ;;
705
+ esac
706
+ }
707
+ pip() {
708
+ if [ "${1:-}" = "install" ] && [ -z "${VIRTUAL_ENV:-}" ] && ! _hc_has_arg --user "$@" && ! _hc_has_arg --prefix "$@"; then
709
+ command pip install --user "${@:2}"
710
+ else
711
+ command pip "$@"
712
+ fi
713
+ local rc=$?
714
+ if [ $rc -eq 0 ] && [ "${1:-}" = "install" ]; then
715
+ _hc_append_cmd "python3 -m pip install --user" "${@:2}"
716
+ fi
717
+ return $rc
718
+ }
719
+ pip3() {
720
+ if [ "${1:-}" = "install" ] && [ -z "${VIRTUAL_ENV:-}" ] && ! _hc_has_arg --user "$@" && ! _hc_has_arg --prefix "$@"; then
721
+ command pip3 install --user "${@:2}"
722
+ else
723
+ command pip3 "$@"
724
+ fi
725
+ local rc=$?
726
+ if [ $rc -eq 0 ] && [ "${1:-}" = "install" ]; then
727
+ _hc_append_cmd "python3 -m pip install --user" "${@:2}"
728
+ fi
729
+ return $rc
730
+ }
731
+ npm() {
732
+ command npm "$@"
733
+ local rc=$?
734
+ if [ $rc -eq 0 ] && [ "${1:-}" = "install" ] && { [ "${2:-}" = "-g" ] || [ "${2:-}" = "--global" ]; }; then
735
+ _hc_append_cmd "npm install -g" "${@:3}"
736
+ fi
737
+ return $rc
738
+ }
739
+ openclaw() {
740
+ command openclaw "$@"
741
+ local rc=$?
742
+ if [ $rc -eq 0 ] && [ "${1:-}" = "plugins" ] && [ "${2:-}" = "install" ]; then
743
+ _hc_allow_openclaw_plugins "${@:3}"
744
+ _hc_append_cmd "openclaw plugins install" "${@:3}"
745
+ fi
746
+ return $rc
747
  }
 
 
 
 
748
  BASHRC
749
+ cat > /home/node/.profile <<'PROFILE'
750
+ [ -f ~/.bashrc ] && . ~/.bashrc
751
+ PROFILE
752
  echo "Shell capture wrappers ready."
753
 
754
  # ── Re-install previously installed plugins ──
 
772
  fi
773
  fi
774
 
775
+ # ── Startup command runner ──
776
+ # Runs user-provided boot commands one by one so failures are visible in logs.
777
+ # By default failures are logged and boot continues; set
778
+ # HUGGINGCLAW_STARTUP_STRICT=true to fail the Space startup on any error.
779
+ HC_STARTUP_FAILURES=0
780
+ HC_STARTUP_STRICT_NORMALIZED=$(printf '%s' "${HUGGINGCLAW_STARTUP_STRICT:-false}" | tr '[:upper:]' '[:lower:]')
781
+ hc_run_startup_command() {
782
+ local source_label="$1"
783
+ local command_text="$2"
784
+ [ -n "$command_text" ] || return 0
785
+
786
+ echo "[startup:${source_label}] $command_text"
787
+ set +e
788
+ bash -lc "$command_text"
789
+ local rc=$?
790
+ set -e
791
+ if [ "$rc" -eq 0 ]; then
792
+ echo "[startup:${source_label}] ok"
793
+ return 0
794
+ fi
795
+
796
+ HC_STARTUP_FAILURES=$((HC_STARTUP_FAILURES + 1))
797
+ echo "ERROR: startup command failed (${source_label}, exit ${rc}): $command_text" >&2
798
+ return "$rc"
799
+ }
800
+
801
+ hc_run_startup_script() {
802
+ local source_label="$1"
803
+ local script_text="$2"
804
+ [ -n "$script_text" ] || return 0
805
+
806
+ local script_file
807
+ script_file=$(mktemp "/tmp/huggingclaw-startup-${source_label//[^A-Za-z0-9_.-]/_}.XXXXXX.sh")
808
+ {
809
+ # Load HuggingClaw's install wrappers for env-provided scripts too, so
810
+ # `apt install`, `pip install`, `npm install -g`, and OpenClaw plugin
811
+ # installs behave the same way as they do in the interactive shell.
812
+ echo '[ -f /home/node/.bashrc ] && . /home/node/.bashrc'
813
+ printf '%s\n' "$script_text"
814
+ } > "$script_file"
815
+ chmod 700 "$script_file"
816
+
817
+ echo "[startup:${source_label}] running script (${script_file})"
818
+ set +e
819
+ bash "$script_file"
820
+ local rc=$?
821
+ set -e
822
+ rm -f "$script_file"
823
+
824
+ if [ "$rc" -eq 0 ]; then
825
+ echo "[startup:${source_label}] ok"
826
+ return 0
827
+ fi
828
+
829
+ HC_STARTUP_FAILURES=$((HC_STARTUP_FAILURES + 1))
830
+ echo "ERROR: startup script failed (${source_label}, exit ${rc})" >&2
831
+ return "$rc"
832
+ }
833
+ hc_run_startup_script_b64() {
834
+ local source_label="$1"
835
+ local encoded_script="$2"
836
+ [ -n "$encoded_script" ] || return 0
837
+
838
+ local script_text
839
+ if ! script_text=$(printf '%s' "$encoded_script" | base64 -d 2>/dev/null); then
840
+ HC_STARTUP_FAILURES=$((HC_STARTUP_FAILURES + 1))
841
+ echo "ERROR: startup script base64 decode failed (${source_label})" >&2
842
+ return 1
843
+ fi
844
+
845
+ hc_run_startup_script "$source_label" "$script_text"
846
+ }
847
+
848
+
849
+ hc_run_startup_auto() {
850
+ local source_label="$1"
851
+ local payload="$2"
852
+ [ -n "$payload" ] || return 0
853
+
854
+ if [[ "$payload" == base64:* ]]; then
855
+ hc_run_startup_script_b64 "$source_label" "${payload#base64:}"
856
+ elif [[ "$payload" == b64:* ]]; then
857
+ hc_run_startup_script_b64 "$source_label" "${payload#b64:}"
858
+ else
859
+ hc_run_startup_script "$source_label" "$payload"
860
+ fi
861
+ }
862
+
863
+ hc_run_command_block() {
864
+ local source_label="$1"
865
+ local command_block="$2"
866
+ local line
867
+ local index=0
868
+
869
+ while IFS= read -r line || [ -n "$line" ]; do
870
+ # Skip blank lines and comments so multi-line env vars can be documented.
871
+ [[ "$line" =~ ^[[:space:]]*$ ]] && continue
872
+ [[ "$line" =~ ^[[:space:]]*# ]] && continue
873
+
874
+ index=$((index + 1))
875
+ hc_run_startup_command "${source_label}[${index}]" "$line" || true
876
+ done <<< "$command_block"
877
+ }
878
+
879
+ sync_installed_plugins_into_allow() {
880
+ local config="/home/node/.openclaw/openclaw.json"
881
+ [ -f "$config" ] || return 0
882
+
883
+ local patched
884
+ patched=$(jq '
885
+ (.plugins.installs // {}) as $installs
886
+ | ($installs | keys) as $installed
887
+ | ($installed | map(if startswith("@openclaw/") then sub("^@openclaw/"; "") else . end)) as $short
888
+ | .plugins.allow = (((.plugins.allow // []) + $installed + $short) | unique)
889
+ ' "$config" 2>/dev/null) || {
890
+ echo "Warning: could not sync installed plugins into plugins.allow"
891
+ return 0
892
+ }
893
+
894
+ echo "$patched" > "$config.tmp" && mv "$config.tmp" "$config"
895
+ }
896
+
897
+ hc_finish_startup_commands() {
898
+ if [ "$HC_STARTUP_FAILURES" -gt 0 ]; then
899
+ echo "ERROR: ${HC_STARTUP_FAILURES} startup command(s) failed. Check the log lines above." >&2
900
+ if [ "$HC_STARTUP_STRICT_NORMALIZED" = "true" ] || [ "$HC_STARTUP_STRICT_NORMALIZED" = "1" ] || [ "$HC_STARTUP_STRICT_NORMALIZED" = "yes" ]; then
901
+ echo "ERROR: HUGGINGCLAW_STARTUP_STRICT=true, stopping startup." >&2
902
+ exit 1
903
+ fi
904
+ fi
905
+ return 0
906
+ }
907
+
908
+ # ── Optional package install lists from HF Variables/Secrets ──
909
+ # These install package names every boot without persisting package files.
910
+ # Use them when you prefer HF Variables over editing workspace/startup.sh.
911
+ if [ -n "${HUGGINGCLAW_APT_PACKAGES:-}" ]; then
912
+ echo "Installing apt packages from HUGGINGCLAW_APT_PACKAGES..."
913
+ read -r -a HC_APT_PACKAGES <<< "$HUGGINGCLAW_APT_PACKAGES"
914
+ if command -v sudo >/dev/null 2>&1; then
915
+ if sudo apt-get update && sudo apt-get install -y "${HC_APT_PACKAGES[@]}"; then
916
+ echo "HUGGINGCLAW_APT_PACKAGES install complete."
917
+ else
918
+ HC_STARTUP_FAILURES=$((HC_STARTUP_FAILURES + 1))
919
+ echo "ERROR: HUGGINGCLAW_APT_PACKAGES install failed: ${HUGGINGCLAW_APT_PACKAGES}" >&2
920
+ fi
921
+ else
922
+ HC_STARTUP_FAILURES=$((HC_STARTUP_FAILURES + 1))
923
+ echo "ERROR: sudo is unavailable; HUGGINGCLAW_APT_PACKAGES install skipped" >&2
924
+ fi
925
+ fi
926
+ if [ -n "${HUGGINGCLAW_PIP_PACKAGES:-}" ]; then
927
+ echo "Installing Python packages from HUGGINGCLAW_PIP_PACKAGES..."
928
+ read -r -a HC_PIP_PACKAGES <<< "$HUGGINGCLAW_PIP_PACKAGES"
929
+ if python3 -m pip install --user "${HC_PIP_PACKAGES[@]}"; then
930
+ echo "HUGGINGCLAW_PIP_PACKAGES install complete."
931
+ else
932
+ HC_STARTUP_FAILURES=$((HC_STARTUP_FAILURES + 1))
933
+ echo "ERROR: HUGGINGCLAW_PIP_PACKAGES install failed: ${HUGGINGCLAW_PIP_PACKAGES}" >&2
934
+ fi
935
+ fi
936
+ if [ -n "${HUGGINGCLAW_NPM_PACKAGES:-}" ]; then
937
+ echo "Installing global npm packages from HUGGINGCLAW_NPM_PACKAGES..."
938
+ read -r -a HC_NPM_PACKAGES <<< "$HUGGINGCLAW_NPM_PACKAGES"
939
+ if npm install -g "${HC_NPM_PACKAGES[@]}"; then
940
+ echo "HUGGINGCLAW_NPM_PACKAGES install complete."
941
+ else
942
+ HC_STARTUP_FAILURES=$((HC_STARTUP_FAILURES + 1))
943
+ echo "ERROR: HUGGINGCLAW_NPM_PACKAGES install failed: ${HUGGINGCLAW_NPM_PACKAGES}" >&2
944
+ fi
945
+ fi
946
+ if [ -n "${HUGGINGCLAW_OPENCLAW_PLUGINS:-}" ]; then
947
+ echo "Installing OpenClaw plugins from HUGGINGCLAW_OPENCLAW_PLUGINS..."
948
+ read -r -a HC_OPENCLAW_PLUGINS <<< "$HUGGINGCLAW_OPENCLAW_PLUGINS"
949
+ if openclaw plugins install "${HC_OPENCLAW_PLUGINS[@]}"; then
950
+ echo "HUGGINGCLAW_OPENCLAW_PLUGINS install complete."
951
+ else
952
+ HC_STARTUP_FAILURES=$((HC_STARTUP_FAILURES + 1))
953
+ echo "ERROR: HUGGINGCLAW_OPENCLAW_PLUGINS install failed: ${HUGGINGCLAW_OPENCLAW_PLUGINS}" >&2
954
+ fi
955
+ fi
956
+
957
+ # ── Arbitrary startup commands from HF Variables/Secrets ──
958
+ # Recommended: use one variable, HUGGINGCLAW_RUN, as a full bash script. If the
959
+ # value starts with base64: or b64:, the rest is decoded and run as the script.
960
+ # Legacy granular HUGGINGCLAW_STARTUP_* variables are still supported below.
961
+ if [ -n "${HUGGINGCLAW_RUN:-}" ]; then
962
+ hc_run_startup_auto "HUGGINGCLAW_RUN" "$HUGGINGCLAW_RUN" || true
963
+ fi
964
+ if [ -n "${HUGGINGCLAW_STARTUP_COMMANDS:-}" ]; then
965
+ echo "Running commands from HUGGINGCLAW_STARTUP_COMMANDS..."
966
+ hc_run_command_block "HUGGINGCLAW_STARTUP_COMMANDS" "$HUGGINGCLAW_STARTUP_COMMANDS"
967
+ fi
968
+ for HC_STARTUP_INDEX in $(seq 1 100); do
969
+ HC_STARTUP_VAR="HUGGINGCLAW_STARTUP_COMMAND_${HC_STARTUP_INDEX}"
970
+ if [ -n "${!HC_STARTUP_VAR:-}" ]; then
971
+ hc_run_startup_command "$HC_STARTUP_VAR" "${!HC_STARTUP_VAR}" || true
972
+ fi
973
+ done
974
+ if [ -n "${HUGGINGCLAW_STARTUP_SCRIPT:-}" ]; then
975
+ hc_run_startup_script "HUGGINGCLAW_STARTUP_SCRIPT" "$HUGGINGCLAW_STARTUP_SCRIPT" || true
976
+ fi
977
+ if [ -n "${HUGGINGCLAW_STARTUP_SCRIPT_B64:-}" ]; then
978
+ hc_run_startup_script_b64 "HUGGINGCLAW_STARTUP_SCRIPT_B64" "$HUGGINGCLAW_STARTUP_SCRIPT_B64" || true
979
+ fi
980
+ for HC_STARTUP_INDEX in $(seq 1 20); do
981
+ HC_STARTUP_VAR="HUGGINGCLAW_STARTUP_SCRIPT_B64_${HC_STARTUP_INDEX}"
982
+ if [ -n "${!HC_STARTUP_VAR:-}" ]; then
983
+ hc_run_startup_script_b64 "$HC_STARTUP_VAR" "${!HC_STARTUP_VAR}" || true
984
+ fi
985
+ done
986
+
987
  # ── Run workspace startup script ──
988
  STARTUP_FILE="/home/node/.openclaw/workspace/startup.sh"
989
  if [ ! -f "$STARTUP_FILE" ]; then
 
992
  echo "Created workspace/startup.sh"
993
  fi
994
  if [ -s "$STARTUP_FILE" ]; then
995
+ echo "Running workspace/startup.sh script..."
996
+ hc_run_startup_script "workspace/startup.sh" "$(cat "$STARTUP_FILE")" || true
997
+ echo "Workspace startup script complete."
998
  fi
999
+ hc_finish_startup_commands
1000
+ sync_installed_plugins_into_allow
1001
 
1002
  # ── Launch gateway ──
1003
  echo "Launching OpenClaw gateway on port 7860..."