| #!/bin/bash |
| set -euo pipefail |
|
|
| umask 0077 |
|
|
| |
| |
| |
|
|
| |
| trim_var() { |
| |
| printf '%s' "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' |
| } |
|
|
| hc_is_true() { |
| case "$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')" in |
| 1|true|yes|on) return 0 ;; |
| *) return 1 ;; |
| esac |
| } |
|
|
| load_env_bundle() { |
| |
| |
| local bundle="${HUGGINGCLAW_ENV_BUNDLE:-${ENV_BUNDLE:-}}" |
| [ -n "$bundle" ] || return 0 |
| eval "$(HUGGINGCLAW_ENV_BUNDLE="$bundle" python3 - <<'PYBUNDLE' |
| import base64, json, os, re, shlex, sys |
| |
| raw = os.environ.get("HUGGINGCLAW_ENV_BUNDLE", "").strip() |
| try: |
| if raw.startswith("{"): |
| data = json.loads(raw) |
| else: |
| padded = raw + "=" * (-len(raw) % 4) |
| data = json.loads(base64.urlsafe_b64decode(padded.encode()).decode()) |
| if not isinstance(data, dict): |
| raise ValueError("bundle must decode to a JSON object") |
| for key, value in data.items(): |
| if not re.fullmatch(r"[A-Z_][A-Z0-9_]*", str(key)): |
| continue |
| if str(key) in {"HUGGINGCLAW_ENV_BUNDLE", "ENV_BUNDLE"}: |
| continue |
| if str(key) == "OPENCLAW_VERSION": |
| print("Warning: OPENCLAW_VERSION from env bundle is ignored (build-time only; set HF Variable and rebuild).", file=sys.stderr) |
| continue |
| if os.environ.get(str(key), ""): |
| continue |
| if value is None or isinstance(value, (dict, list)): |
| continue |
| print(f"export {key}={shlex.quote(str(value))}") |
| except Exception as exc: |
| print(f"Warning: invalid HUGGINGCLAW_ENV_BUNDLE ignored: {exc}", file=sys.stderr) |
| PYBUNDLE |
| )" |
| } |
|
|
| load_env_bundle |
|
|
| |
| |
| LLM_MODEL="$(trim_var "${LLM_MODEL:-}")" |
| GATEWAY_TOKEN="$(trim_var "${GATEWAY_TOKEN:-}")" |
| OPENCLAW_PASSWORD="$(trim_var "${OPENCLAW_PASSWORD:-}")" |
| LLM_API_KEY="$(trim_var "${LLM_API_KEY:-}")" |
| CLOUDFLARE_PROXY_URL="$(trim_var "${CLOUDFLARE_PROXY_URL:-}")" |
|
|
| OPENCLAW_VERSION="${OPENCLAW_VERSION:-latest}" |
| APP_BASE="$(trim_var "${APP_BASE:-/app}")" |
| JUPYTER_BASE="$(trim_var "${JUPYTER_BASE:-/terminal}")" |
| PORT="$(trim_var "${PORT:-7861}")" |
| GATEWAY_PORT="$(trim_var "${GATEWAY_PORT:-7860}")" |
| JUPYTER_PORT="$(trim_var "${JUPYTER_PORT:-8888}")" |
| BACKUP_DATASET_NAME="$(trim_var "${BACKUP_DATASET_NAME:-${BACKUP_DATASET:-huggingclaw-backup}}")" |
| SPACE_AUTHOR_NAME="$(trim_var "${SPACE_AUTHOR_NAME:-}")" |
| SPACE_HOST="$(trim_var "${SPACE_HOST:-}")" |
| OPENCLAW_APP_DIR="/home/node/.openclaw/openclaw-app" |
| OPENCLAW_RUNTIME_VERSION="" |
| OPENCLAW_FILE_LOG_LEVEL_CONFIGURED=false |
| OPENCLAW_CONSOLE_LOG_LEVEL_CONFIGURED=false |
| OPENCLAW_CONSOLE_LOG_STYLE_CONFIGURED=false |
| WHATSAPP_ENABLED_CONFIGURED=false |
| [ "${OPENCLAW_FILE_LOG_LEVEL+x}" = "x" ] && OPENCLAW_FILE_LOG_LEVEL_CONFIGURED=true |
| [ "${OPENCLAW_CONSOLE_LOG_LEVEL+x}" = "x" ] && OPENCLAW_CONSOLE_LOG_LEVEL_CONFIGURED=true |
| [ "${OPENCLAW_CONSOLE_LOG_STYLE+x}" = "x" ] && OPENCLAW_CONSOLE_LOG_STYLE_CONFIGURED=true |
| [ "${WHATSAPP_ENABLED+x}" = "x" ] && WHATSAPP_ENABLED_CONFIGURED=true |
| WHATSAPP_ENABLED="${WHATSAPP_ENABLED:-false}" |
| WHATSAPP_ENABLED_NORMALIZED=$(printf '%s' "$WHATSAPP_ENABLED" | tr '[:upper:]' '[:lower:]') |
| DEV_MODE_RAW="${DEV_MODE:-false}" |
| DEV_MODE_NORMALIZED=$(printf '%s' "$DEV_MODE_RAW" | tr '[:upper:]' '[:lower:]') |
| DEV_MODE_ENABLED=false |
| if hc_is_true "$DEV_MODE_NORMALIZED"; then |
| DEV_MODE_ENABLED=true |
| fi |
| |
| |
| if [ "$DEV_MODE_ENABLED" != "true" ] && [ -z "${DEV_MODE:-}" ] && [ -n "${GATEWAY_TOKEN:-}" ]; then |
| DEV_MODE_ENABLED=true |
| : |
| fi |
| if [ "$DEV_MODE_ENABLED" = "true" ]; then |
| export DEV_MODE=true |
| else |
| export DEV_MODE=false |
| fi |
| SYNC_INTERVAL="$(trim_var "${SYNC_INTERVAL:-180}")" |
| DEVDATA_DATASET_NAME="$(trim_var "${DEVDATA_DATASET_NAME:-huggingclaw-devdata}")" |
| DEVDATA_SYNC_INTERVAL="$(trim_var "${DEVDATA_SYNC_INTERVAL:-180}")" |
| DEVDATA_RAW="$(trim_var "${DEVDATA:-on}")" |
| DEVDATA_NORMALIZED=$(printf '%s' "$DEVDATA_RAW" | tr '[:upper:]' '[:lower:]') |
| DEVDATA_ENABLED=true |
| if ! hc_is_true "$DEVDATA_NORMALIZED"; then |
| DEVDATA_ENABLED=false |
| fi |
| |
| |
| |
| if [ -n "${SPACE_HOST:-}" ]; then |
| OPENCLAW_CONSOLE_LOG_LEVEL="${OPENCLAW_CONSOLE_LOG_LEVEL:-warn}" |
| OPENCLAW_FILE_LOG_LEVEL="${OPENCLAW_FILE_LOG_LEVEL:-info}" |
| OPENCLAW_CONSOLE_LOG_STYLE="${OPENCLAW_CONSOLE_LOG_STYLE:-compact}" |
| BROWSER_PLUGIN_MODE="${BROWSER_PLUGIN_MODE:-disabled}" |
| ACP_PLUGIN_MODE="${ACP_PLUGIN_MODE:-disabled}" |
| |
| export OPENCLAW_DISABLE_BONJOUR="${OPENCLAW_DISABLE_BONJOUR:-1}" |
| else |
| OPENCLAW_CONSOLE_LOG_LEVEL="${OPENCLAW_CONSOLE_LOG_LEVEL:-info}" |
| OPENCLAW_FILE_LOG_LEVEL="${OPENCLAW_FILE_LOG_LEVEL:-info}" |
| OPENCLAW_CONSOLE_LOG_STYLE="${OPENCLAW_CONSOLE_LOG_STYLE:-pretty}" |
| BROWSER_PLUGIN_MODE="${BROWSER_PLUGIN_MODE:-auto}" |
| ACP_PLUGIN_MODE="${ACP_PLUGIN_MODE:-auto}" |
| fi |
| echo "" |
| echo " ββββββββββββββββββββββββββββββββββββββββββββ" |
| echo " β π¦ HuggingClaw + π» JupyterLab β" |
| echo " ββββββββββββββββββββββββββββββββββββββββββββ" |
| echo "" |
|
|
| |
| ERRORS="" |
| if [ -z "$LLM_API_KEY" ]; then |
| ERRORS="${ERRORS} - LLM_API_KEY is not set\n" |
| fi |
| if [ -z "$LLM_MODEL" ]; then |
| ERRORS="${ERRORS} - LLM_MODEL is not set (e.g. google/gemini-2.5-flash, anthropic/claude-sonnet-4-5, openai/gpt-4)\n" |
| fi |
| if [ -z "$GATEWAY_TOKEN" ]; then |
| ERRORS="${ERRORS} - GATEWAY_TOKEN is not set (generate: openssl rand -hex 32)\n" |
| fi |
| if [ -n "$ERRORS" ]; then |
| echo "Missing required secrets:" |
| echo -e "$ERRORS" |
| echo "Add them in HF Spaces β Settings β Secrets" |
| exit 1 |
| fi |
|
|
| |
| |
| if [ -f "$OPENCLAW_APP_DIR/package.json" ]; then |
| OPENCLAW_RUNTIME_VERSION=$(node -p "require('$OPENCLAW_APP_DIR/package.json').version" 2>/dev/null || true) |
| fi |
|
|
| if [ -n "$OPENCLAW_RUNTIME_VERSION" ]; then |
| OPENCLAW_DISPLAY_VERSION="$OPENCLAW_RUNTIME_VERSION" |
| if [ "$OPENCLAW_VERSION" != "$OPENCLAW_RUNTIME_VERSION" ]; then |
| OPENCLAW_DISPLAY_VERSION="$OPENCLAW_RUNTIME_VERSION (tag: $OPENCLAW_VERSION)" |
| fi |
| else |
| OPENCLAW_DISPLAY_VERSION="$OPENCLAW_VERSION" |
| fi |
|
|
| |
|
|
| |
| if [[ "$LLM_MODEL" == "anthropic/gemini"* ]]; then |
| LLM_MODEL=$(echo "$LLM_MODEL" | sed 's/^anthropic\//google\//') |
| echo "Note: corrected model from anthropic/gemini* to google/gemini*" |
| fi |
|
|
| |
| LLM_PROVIDER=$(echo "$LLM_MODEL" | cut -d'/' -f1) |
|
|
| |
| |
| |
| case "$LLM_PROVIDER" in |
| |
| anthropic) export ANTHROPIC_API_KEY="$LLM_API_KEY" ;; |
| openai|openai-codex) export OPENAI_API_KEY="$LLM_API_KEY" ;; |
| google|google-vertex) export GEMINI_API_KEY="$LLM_API_KEY" ;; |
| deepseek) export DEEPSEEK_API_KEY="$LLM_API_KEY" ;; |
| |
| opencode) export OPENCODE_API_KEY="$LLM_API_KEY" ;; |
| opencode-go) export OPENCODE_API_KEY="$LLM_API_KEY" ;; |
| |
| openrouter) export OPENROUTER_API_KEY="$LLM_API_KEY" ;; |
| kilocode) export KILOCODE_API_KEY="$LLM_API_KEY" ;; |
| vercel-ai-gateway) export AI_GATEWAY_API_KEY="$LLM_API_KEY" ;; |
| |
| zai|z-ai|z.ai|zhipu) export ZAI_API_KEY="$LLM_API_KEY" ;; |
| moonshot) export MOONSHOT_API_KEY="$LLM_API_KEY" ;; |
| kimi-coding) export KIMI_API_KEY="$LLM_API_KEY" ;; |
| minimax) export MINIMAX_API_KEY="$LLM_API_KEY" ;; |
| qwen|modelstudio) export MODELSTUDIO_API_KEY="$LLM_API_KEY" ;; |
| xiaomi) export XIAOMI_API_KEY="$LLM_API_KEY" ;; |
| volcengine|volcengine-plan) export VOLCANO_ENGINE_API_KEY="$LLM_API_KEY" ;; |
| byteplus|byteplus-plan) export BYTEPLUS_API_KEY="$LLM_API_KEY" ;; |
| qianfan) export QIANFAN_API_KEY="$LLM_API_KEY" ;; |
| |
| mistral) export MISTRAL_API_KEY="$LLM_API_KEY" ;; |
| xai|x-ai) export XAI_API_KEY="$LLM_API_KEY" ;; |
| nvidia) export NVIDIA_API_KEY="$LLM_API_KEY" ;; |
| cohere) export COHERE_API_KEY="$LLM_API_KEY" ;; |
| groq) export GROQ_API_KEY="$LLM_API_KEY" ;; |
| together) export TOGETHER_API_KEY="$LLM_API_KEY" ;; |
| huggingface) export HUGGINGFACE_HUB_TOKEN="$LLM_API_KEY" ;; |
| cerebras) export CEREBRAS_API_KEY="$LLM_API_KEY" ;; |
| venice) export VENICE_API_KEY="$LLM_API_KEY" ;; |
| synthetic) export SYNTHETIC_API_KEY="$LLM_API_KEY" ;; |
| github-copilot) export COPILOT_GITHUB_TOKEN="$LLM_API_KEY" ;; |
| llama-3.*|llama-4.*|mixtral-*|gemma-*) |
| export GROQ_API_KEY="$LLM_API_KEY" |
| echo "Note: bare Groq model '$LLM_MODEL' detected; mapped LLM_API_KEY β GROQ_API_KEY. Use 'groq/${LLM_MODEL}' prefix to be explicit." ;; |
| mistral-*|codestral-*|devstral-*|voxtral-*) |
| export MISTRAL_API_KEY="$LLM_API_KEY" |
| echo "Note: bare Mistral model '$LLM_MODEL' detected; mapped LLM_API_KEY β MISTRAL_API_KEY. Use 'mistral/${LLM_MODEL}' prefix to be explicit." ;; |
| moonshotai|meta-llama|deepseek-ai|MiniMaxAI|minimax-ai|Qwen|zai-org|mistralai|google) |
| echo "Warning: LLM_MODEL='$LLM_MODEL' uses sub-provider prefix '$LLM_PROVIDER'. This is a router-namespaced model (Together/OpenRouter). Mapping LLM_API_KEY β TOGETHER_API_KEY. If using OpenRouter, also set OPENROUTER_API_KEY as a separate secret." |
| export TOGETHER_API_KEY="${TOGETHER_API_KEY:-$LLM_API_KEY}" ;; |
| |
| *) |
| echo "Warning: Unknown provider prefix '$LLM_PROVIDER' in LLM_MODEL='$LLM_MODEL'. Defaulting to ANTHROPIC_API_KEY. If using a router-namespaced model (e.g. moonshotai/Kimi-K2.5), set TOGETHER_API_KEY or OPENROUTER_API_KEY as a separate secret." |
| export ANTHROPIC_API_KEY="$LLM_API_KEY" |
| ;; |
| esac |
|
|
| |
| |
| promote_first_pool_key() { |
| local singular_var="$1" |
| local pool_var="$2" |
| local singular_val="${!singular_var:-}" |
| local pool_val="${!pool_var:-}" |
|
|
| [ -n "$singular_val" ] && return 0 |
| [ -n "$pool_val" ] || return 0 |
|
|
| local first |
| first=$(printf '%s' "$pool_val" | tr ',' '\n' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | awk 'NF{print; exit}') |
| [ -n "$first" ] || return 0 |
| export "${singular_var}=$first" |
| } |
|
|
| promote_first_pool_key "ANTHROPIC_API_KEY" "ANTHROPIC_API_KEYS" |
| promote_first_pool_key "OPENAI_API_KEY" "OPENAI_API_KEYS" |
| promote_first_pool_key "GEMINI_API_KEY" "GEMINI_API_KEYS" |
| promote_first_pool_key "DEEPSEEK_API_KEY" "DEEPSEEK_API_KEYS" |
| promote_first_pool_key "OPENROUTER_API_KEY" "OPENROUTER_API_KEYS" |
| promote_first_pool_key "KILOCODE_API_KEY" "KILOCODE_API_KEYS" |
| promote_first_pool_key "OPENCODE_API_KEY" "OPENCODE_API_KEYS" |
| promote_first_pool_key "ZAI_API_KEY" "ZAI_API_KEYS" |
| promote_first_pool_key "MOONSHOT_API_KEY" "MOONSHOT_API_KEYS" |
| promote_first_pool_key "MINIMAX_API_KEY" "MINIMAX_API_KEYS" |
| promote_first_pool_key "XIAOMI_API_KEY" "XIAOMI_API_KEYS" |
| promote_first_pool_key "VOLCANO_ENGINE_API_KEY" "VOLCANO_ENGINE_API_KEYS" |
| promote_first_pool_key "BYTEPLUS_API_KEY" "BYTEPLUS_API_KEYS" |
| promote_first_pool_key "QIANFAN_API_KEY" "QIANFAN_API_KEYS" |
| promote_first_pool_key "MODELSTUDIO_API_KEY" "MODELSTUDIO_API_KEYS" |
| promote_first_pool_key "KIMI_API_KEY" "KIMI_API_KEYS" |
| promote_first_pool_key "MISTRAL_API_KEY" "MISTRAL_API_KEYS" |
| promote_first_pool_key "XAI_API_KEY" "XAI_API_KEYS" |
| promote_first_pool_key "NVIDIA_API_KEY" "NVIDIA_API_KEYS" |
| promote_first_pool_key "GROQ_API_KEY" "GROQ_API_KEYS" |
| promote_first_pool_key "COHERE_API_KEY" "COHERE_API_KEYS" |
| promote_first_pool_key "TOGETHER_API_KEY" "TOGETHER_API_KEYS" |
| promote_first_pool_key "CEREBRAS_API_KEY" "CEREBRAS_API_KEYS" |
| promote_first_pool_key "VENICE_API_KEY" "VENICE_API_KEYS" |
| promote_first_pool_key "SYNTHETIC_API_KEY" "SYNTHETIC_API_KEYS" |
| promote_first_pool_key "COPILOT_GITHUB_TOKEN" "COPILOT_GITHUB_TOKENS" |
| promote_first_pool_key "AI_GATEWAY_API_KEY" "AI_GATEWAY_API_KEYS" |
|
|
| |
| |
| |
| if [ -z "${MOONSHOT_API_KEY:-}" ] && [ -n "${KIMI_API_KEY:-}" ]; then |
| export MOONSHOT_API_KEY="$KIMI_API_KEY" |
| fi |
| promote_first_pool_key "HUGGINGFACE_HUB_TOKEN" "HUGGINGFACE_HUB_TOKENS" |
|
|
| |
| mkdir -p /home/node/.openclaw/agents/main/sessions |
| mkdir -p /home/node/.openclaw/credentials |
| mkdir -p /home/node/.openclaw/memory |
| mkdir -p /home/node/.openclaw/extensions |
| mkdir -p /home/node/.openclaw/workspace |
| mkdir -p /home/node/.local/bin /home/node/.local/lib /home/node/.npm-global |
| chmod 700 /home/node/.openclaw |
| chmod 700 /home/node/.openclaw/credentials |
|
|
| |
| |
| |
| |
| export NPM_CONFIG_PREFIX="${NPM_CONFIG_PREFIX:-/home/node/.local}" |
| export npm_config_prefix="$NPM_CONFIG_PREFIX" |
| export PYTHONUSERBASE="${PYTHONUSERBASE:-/home/node/.local}" |
| export DEBIAN_FRONTEND="${DEBIAN_FRONTEND:-noninteractive}" |
| |
| |
| if [ -z "${PS1:-}" ] || [ "$PS1" = "$ " ]; then |
| export PS1='\u@\h:\w\$ ' |
| fi |
| STARTUP_FILE="/home/node/.openclaw/workspace/startup.sh" |
|
|
| |
| BACKUP_DATASET="${BACKUP_DATASET_NAME:-huggingclaw-backup}" |
| if [ -n "${HF_TOKEN:-}" ]; then |
| echo "Restoring workspace from HF Dataset..." |
| python3 /home/node/app/openclaw-sync.py restore || true |
| else |
| echo "HF_TOKEN not set β running without dataset persistence." |
| fi |
|
|
| CLOUDFLARE_WORKERS_TOKEN="${CLOUDFLARE_WORKERS_TOKEN:-}" |
| export CLOUDFLARE_WORKERS_TOKEN |
| CF_PROXY_ENV_FILE="/tmp/huggingclaw-cloudflare-proxy.env" |
| if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ] || [ -n "${CLOUDFLARE_PROXY_URL:-}" ]; then |
| |
| |
| export CLOUDFLARE_PROXY_DEBUG="${CLOUDFLARE_PROXY_DEBUG:-false}" |
| echo "Preparing Cloudflare outbound proxy..." |
| python3 /home/node/app/cloudflare-proxy-setup.py || true |
| if [ -f "$CF_PROXY_ENV_FILE" ]; then |
| . "$CF_PROXY_ENV_FILE" |
| fi |
| fi |
|
|
| |
| CONFIG_JSON=$(cat <<'CONFIGEOF' |
| { |
| "gateway": { |
| "mode": "local", |
| "port": "${GATEWAY_PORT}", |
| "bind": "lan", |
| "auth": { |
| "token": "" |
| }, |
| "controlUi": { |
| "allowInsecureAuth": true, |
| "basePath": "/app" |
| }, |
| "trustedProxies": ["127.0.0.1/8", "::1/128", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] |
| }, |
| "channels": {}, |
| "plugins": { |
| "entries": {} |
| }, |
| "logging": { |
| "level": "info", |
| "consoleLevel": "warn", |
| "consoleStyle": "compact" |
| } |
| } |
| CONFIGEOF |
| ) |
|
|
| |
| |
| |
| CONFIG_JSON=$(jq \ |
| --arg token "$GATEWAY_TOKEN" \ |
| --arg model "$LLM_MODEL" \ |
| --arg fileLevel "$OPENCLAW_FILE_LOG_LEVEL" \ |
| --arg consoleLevel "$OPENCLAW_CONSOLE_LOG_LEVEL" \ |
| --arg consoleStyle "$OPENCLAW_CONSOLE_LOG_STYLE" \ |
| --arg port "$GATEWAY_PORT" \ |
| '.gateway.auth.token = $token |
| | .agents.defaults.model = $model |
| | .gateway.port = ($port | tonumber) |
| | .logging.level = $fileLevel |
| | .logging.consoleLevel = $consoleLevel |
| | .logging.consoleStyle = $consoleStyle' <<<"$CONFIG_JSON") |
|
|
| |
| CUSTOM_PROVIDER_NAME="${CUSTOM_PROVIDER_NAME:-}" |
| CUSTOM_BASE_URL="${CUSTOM_BASE_URL:-}" |
| CUSTOM_MODEL_ID="${CUSTOM_MODEL_ID:-}" |
| CUSTOM_MODEL_NAME="${CUSTOM_MODEL_NAME:-$CUSTOM_MODEL_ID}" |
| CUSTOM_API_KEY="${CUSTOM_API_KEY:-$LLM_API_KEY}" |
| CUSTOM_API_TYPE="${CUSTOM_API_TYPE:-openai-completions}" |
| CUSTOM_CONTEXT_WINDOW="${CUSTOM_CONTEXT_WINDOW:-128000}" |
| CUSTOM_MAX_TOKENS="${CUSTOM_MAX_TOKENS:-8192}" |
|
|
| if [ -n "$CUSTOM_PROVIDER_NAME" ] || [ -n "$CUSTOM_BASE_URL" ] || [ -n "$CUSTOM_MODEL_ID" ]; then |
| CUSTOM_PROVIDER_NORMALIZED=$(printf '%s' "$CUSTOM_PROVIDER_NAME" | tr '[:upper:]' '[:lower:]') |
| CUSTOM_BASE_URL_NORMALIZED="${CUSTOM_BASE_URL%/}" |
| CUSTOM_PROVIDER_OK=true |
|
|
| if [ -z "$CUSTOM_PROVIDER_NAME" ] || [ -z "$CUSTOM_BASE_URL" ] || [ -z "$CUSTOM_MODEL_ID" ]; then |
| echo "Warning: custom provider skipped: set CUSTOM_PROVIDER_NAME, CUSTOM_BASE_URL, and CUSTOM_MODEL_ID together." |
| CUSTOM_PROVIDER_OK=false |
| fi |
|
|
| case "$CUSTOM_PROVIDER_NORMALIZED" in |
| anthropic|openai|openai-codex|google|google-vertex|deepseek|opencode|opencode-go|openrouter|kilocode|vercel-ai-gateway|zai|z-ai|z.ai|zhipu|moonshot|kimi-coding|minimax|qwen|modelstudio|xiaomi|volcengine|volcengine-plan|byteplus|byteplus-plan|qianfan|mistral|mistralai|xai|x-ai|nvidia|cohere|groq|together|huggingface|cerebras|venice|synthetic|github-copilot) |
| echo "Warning: custom provider skipped: CUSTOM_PROVIDER_NAME='$CUSTOM_PROVIDER_NAME' conflicts with a built-in provider." |
| CUSTOM_PROVIDER_OK=false |
| ;; |
| esac |
|
|
| if [[ "$CUSTOM_BASE_URL_NORMALIZED" == */chat/completions ]] || [[ "$CUSTOM_BASE_URL_NORMALIZED" == */completions ]]; then |
| echo "Warning: custom provider skipped: CUSTOM_BASE_URL should be the API base URL, not a completions endpoint." |
| CUSTOM_PROVIDER_OK=false |
| fi |
|
|
| if ! [[ "$CUSTOM_CONTEXT_WINDOW" =~ ^[0-9]+$ ]] || ! [[ "$CUSTOM_MAX_TOKENS" =~ ^[0-9]+$ ]]; then |
| echo "Warning: custom provider skipped: CUSTOM_CONTEXT_WINDOW and CUSTOM_MAX_TOKENS must be whole numbers." |
| CUSTOM_PROVIDER_OK=false |
| fi |
|
|
| if [ "$CUSTOM_PROVIDER_OK" = "true" ]; then |
| echo "Registering custom provider: $CUSTOM_PROVIDER_NAME -> $CUSTOM_BASE_URL_NORMALIZED" |
| CONFIG_JSON=$(jq \ |
| --arg provider "$CUSTOM_PROVIDER_NAME" \ |
| --arg baseUrl "$CUSTOM_BASE_URL_NORMALIZED" \ |
| --arg apiKey "$CUSTOM_API_KEY" \ |
| --arg apiType "$CUSTOM_API_TYPE" \ |
| --arg modelId "$CUSTOM_MODEL_ID" \ |
| --arg modelName "$CUSTOM_MODEL_NAME" \ |
| --argjson contextWindow "$CUSTOM_CONTEXT_WINDOW" \ |
| --argjson maxTokens "$CUSTOM_MAX_TOKENS" \ |
| '.models.mode = "merge" | |
| .models.providers[$provider] = { |
| "baseUrl": $baseUrl, |
| "apiKey": $apiKey, |
| "api": $apiType, |
| "models": [{ |
| "id": $modelId, |
| "name": $modelName, |
| "contextWindow": $contextWindow, |
| "maxTokens": $maxTokens |
| }] |
| }' <<<"$CONFIG_JSON") |
|
|
| if [[ "$LLM_MODEL" != "$CUSTOM_PROVIDER_NAME/"* ]]; then |
| echo "Warning: custom provider registered, but LLM_MODEL='$LLM_MODEL' does not start with '$CUSTOM_PROVIDER_NAME/'." |
| fi |
| fi |
| fi |
|
|
| |
| |
| |
| |
| |
| inject_provider_models_from_env() { |
| local provider="$1" |
| local models_env="$2" |
| local key_env_single="$3" |
| local key_env_pool="$4" |
| local models_csv="${!models_env:-}" |
| local single_key="${!key_env_single:-}" |
| local pool_keys="${!key_env_pool:-}" |
|
|
| |
| |
| |
| if [ -z "$models_csv" ] || { [ -z "$single_key" ] && [ -z "$pool_keys" ]; }; then |
| return 0 |
| fi |
|
|
| local models_json |
| models_json=$(printf '%s' "$models_csv" \ |
| | tr ',' '\n' \ |
| | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' \ |
| | awk 'NF' \ |
| | jq -R . \ |
| | jq -s 'map({id: ., name: .}) | unique_by(.id)') |
|
|
| CONFIG_JSON=$(jq \ |
| --arg provider "$provider" \ |
| --argjson models "$models_json" \ |
| 'if .models.providers[$provider] then |
| .models.mode = "merge" |
| | .models.providers[$provider].models = $models |
| else . end' <<<"$CONFIG_JSON") |
| } |
|
|
| |
| inject_provider_models_from_env "anthropic" "ANTHROPIC_MODELS" "ANTHROPIC_API_KEY" "ANTHROPIC_API_KEYS" |
| inject_provider_models_from_env "openai" "OPENAI_MODELS" "OPENAI_API_KEY" "OPENAI_API_KEYS" |
| inject_provider_models_from_env "openai-codex" "OPENAI_MODELS" "OPENAI_API_KEY" "OPENAI_API_KEYS" |
| inject_provider_models_from_env "google" "GEMINI_MODELS" "GEMINI_API_KEY" "GEMINI_API_KEYS" |
| inject_provider_models_from_env "google-vertex" "GEMINI_MODELS" "GEMINI_API_KEY" "GEMINI_API_KEYS" |
| inject_provider_models_from_env "deepseek" "DEEPSEEK_MODELS" "DEEPSEEK_API_KEY" "DEEPSEEK_API_KEYS" |
| inject_provider_models_from_env "openrouter" "OPENROUTER_MODELS" "OPENROUTER_API_KEY" "OPENROUTER_API_KEYS" |
| inject_provider_models_from_env "kilocode" "KILOCODE_MODELS" "KILOCODE_API_KEY" "KILOCODE_API_KEYS" |
| inject_provider_models_from_env "opencode" "OPENCODE_MODELS" "OPENCODE_API_KEY" "OPENCODE_API_KEYS" |
| inject_provider_models_from_env "opencode-go" "OPENCODE_MODELS" "OPENCODE_API_KEY" "OPENCODE_API_KEYS" |
| inject_provider_models_from_env "zai" "ZAI_MODELS" "ZAI_API_KEY" "ZAI_API_KEYS" |
| inject_provider_models_from_env "z-ai" "ZAI_MODELS" "ZAI_API_KEY" "ZAI_API_KEYS" |
| inject_provider_models_from_env "z.ai" "ZAI_MODELS" "ZAI_API_KEY" "ZAI_API_KEYS" |
| inject_provider_models_from_env "zhipu" "ZAI_MODELS" "ZAI_API_KEY" "ZAI_API_KEYS" |
| inject_provider_models_from_env "moonshot" "MOONSHOT_MODELS" "MOONSHOT_API_KEY" "MOONSHOT_API_KEYS" |
| inject_provider_models_from_env "kimi-coding" "KIMI_MODELS" "KIMI_API_KEY" "KIMI_API_KEYS" |
| inject_provider_models_from_env "minimax" "MINIMAX_MODELS" "MINIMAX_API_KEY" "MINIMAX_API_KEYS" |
| inject_provider_models_from_env "modelstudio" "MODELSTUDIO_MODELS" "MODELSTUDIO_API_KEY" "MODELSTUDIO_API_KEYS" |
| inject_provider_models_from_env "qwen" "MODELSTUDIO_MODELS" "MODELSTUDIO_API_KEY" "MODELSTUDIO_API_KEYS" |
| inject_provider_models_from_env "xiaomi" "XIAOMI_MODELS" "XIAOMI_API_KEY" "XIAOMI_API_KEYS" |
| inject_provider_models_from_env "volcengine" "VOLCANO_ENGINE_MODELS" "VOLCANO_ENGINE_API_KEY" "VOLCANO_ENGINE_API_KEYS" |
| inject_provider_models_from_env "volcengine-plan" "VOLCANO_ENGINE_MODELS" "VOLCANO_ENGINE_API_KEY" "VOLCANO_ENGINE_API_KEYS" |
| inject_provider_models_from_env "byteplus" "BYTEPLUS_MODELS" "BYTEPLUS_API_KEY" "BYTEPLUS_API_KEYS" |
| inject_provider_models_from_env "byteplus-plan" "BYTEPLUS_MODELS" "BYTEPLUS_API_KEY" "BYTEPLUS_API_KEYS" |
| inject_provider_models_from_env "qianfan" "QIANFAN_MODELS" "QIANFAN_API_KEY" "QIANFAN_API_KEYS" |
| inject_provider_models_from_env "groq" "GROQ_MODELS" "GROQ_API_KEY" "GROQ_API_KEYS" |
| inject_provider_models_from_env "mistral" "MISTRAL_MODELS" "MISTRAL_API_KEY" "MISTRAL_API_KEYS" |
| inject_provider_models_from_env "mistralai" "MISTRAL_MODELS" "MISTRAL_API_KEY" "MISTRAL_API_KEYS" |
| inject_provider_models_from_env "xai" "XAI_MODELS" "XAI_API_KEY" "XAI_API_KEYS" |
| inject_provider_models_from_env "x-ai" "XAI_MODELS" "XAI_API_KEY" "XAI_API_KEYS" |
| inject_provider_models_from_env "nvidia" "NVIDIA_MODELS" "NVIDIA_API_KEY" "NVIDIA_API_KEYS" |
| inject_provider_models_from_env "cohere" "COHERE_MODELS" "COHERE_API_KEY" "COHERE_API_KEYS" |
| inject_provider_models_from_env "together" "TOGETHER_MODELS" "TOGETHER_API_KEY" "TOGETHER_API_KEYS" |
| inject_provider_models_from_env "cerebras" "CEREBRAS_MODELS" "CEREBRAS_API_KEY" "CEREBRAS_API_KEYS" |
| inject_provider_models_from_env "huggingface" "HUGGINGFACE_MODELS" "HUGGINGFACE_HUB_TOKEN" "HUGGINGFACE_HUB_TOKENS" |
| inject_provider_models_from_env "venice" "VENICE_MODELS" "VENICE_API_KEY" "VENICE_API_KEYS" |
| inject_provider_models_from_env "synthetic" "SYNTHETIC_MODELS" "SYNTHETIC_API_KEY" "SYNTHETIC_API_KEYS" |
| inject_provider_models_from_env "github-copilot" "GITHUB_COPILOT_MODELS" "COPILOT_GITHUB_TOKEN" "COPILOT_GITHUB_TOKENS" |
|
|
| |
| BROWSER_EXECUTABLE_PATH="" |
| BROWSER_WRAPPER_PATH="" |
| HAS_FILE_CMD=false |
| if command -v file >/dev/null 2>&1; then |
| HAS_FILE_CMD=true |
| fi |
|
|
| ensure_chromium_for_browser_plugin() { |
| |
| [ "$BROWSER_PLUGIN_MODE" = "enabled" ] || return 0 |
| for candidate in /usr/lib/chromium/chromium /usr/bin/chromium /usr/bin/chromium-browser; do |
| [ -x "$candidate" ] && return 0 |
| done |
| if [ "$HAS_FILE_CMD" != "true" ]; then |
| echo "BROWSER_PLUGIN_MODE=enabled and 'file' command is missing; attempting runtime install..." |
| if _hc_apt_install file; then |
| HAS_FILE_CMD=true |
| echo "'file' command installed via apt-get." |
| else |
| echo "Warning: could not install 'file'; continuing with executable-path fallback checks." |
| fi |
| fi |
| echo "BROWSER_PLUGIN_MODE=enabled but Chromium is missing; attempting runtime install..." |
| if _hc_apt_install chromium; then |
| echo "Chromium installed via apt-get." |
| return 0 |
| fi |
| if _hc_apt_install chromium-browser; then |
| echo "Chromium browser package installed via apt-get." |
| return 0 |
| fi |
| echo "ERROR: Browser plugin is enabled, but Chromium install failed. Disable browser plugin or rebuild image with Chromium preinstalled." >&2 |
| return 1 |
| } |
| HC_STARTUP_FAILURES=0 |
| ensure_chromium_for_browser_plugin || HC_STARTUP_FAILURES=$((HC_STARTUP_FAILURES + 1)) |
|
|
| |
| |
| |
| for candidate in \ |
| /usr/lib/chromium/chromium \ |
| /usr/lib/chromium-browser/chromium-browser \ |
| /usr/bin/chromium \ |
| /usr/bin/chromium-browser \ |
| /snap/bin/chromium; do |
| if [ -x "$candidate" ]; then |
| if [ "$HAS_FILE_CMD" = "true" ]; then |
| if file "$candidate" 2>/dev/null | grep -q "ELF"; then |
| BROWSER_EXECUTABLE_PATH="$candidate" |
| break |
| fi |
| else |
| |
| BROWSER_EXECUTABLE_PATH="$candidate" |
| break |
| fi |
| if [ -z "$BROWSER_WRAPPER_PATH" ]; then |
| BROWSER_WRAPPER_PATH="$candidate" |
| fi |
| fi |
| done |
| if [ -z "$BROWSER_EXECUTABLE_PATH" ] && [ -n "$BROWSER_WRAPPER_PATH" ]; then |
| BROWSER_EXECUTABLE_PATH="$BROWSER_WRAPPER_PATH" |
| echo "No ELF Chromium binary found; using launcher wrapper at $BROWSER_EXECUTABLE_PATH" |
| elif [ -n "$BROWSER_EXECUTABLE_PATH" ] && [ "$HAS_FILE_CMD" != "true" ]; then |
| echo "Detected Chromium executable at $BROWSER_EXECUTABLE_PATH (ELF probe skipped: 'file' command not installed)" |
| fi |
| if [ -z "$BROWSER_EXECUTABLE_PATH" ]; then |
| echo "Warning: Chromium executable not found. Browser plugin will be disabled." |
| fi |
|
|
| BROWSER_SHOULD_ENABLE=false |
| if [ "$BROWSER_PLUGIN_MODE" = "enabled" ] && [ -n "$BROWSER_EXECUTABLE_PATH" ] && [ -x "$BROWSER_EXECUTABLE_PATH" ]; then |
| BROWSER_SHOULD_ENABLE=true |
| elif [ "$BROWSER_PLUGIN_MODE" = "auto" ] && [ -n "$BROWSER_EXECUTABLE_PATH" ] && [ -x "$BROWSER_EXECUTABLE_PATH" ]; then |
| BROWSER_SHOULD_ENABLE=true |
| fi |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| PLUGIN_ALLOW_JSON='["device-pair","phone-control","talk-voice"]' |
| if [ "$ACP_PLUGIN_MODE" = "enabled" ] || [ "$ACP_PLUGIN_MODE" = "auto" ]; then |
| PLUGIN_ALLOW_JSON=$(jq '. + ["acpx"]' <<<"$PLUGIN_ALLOW_JSON") |
| fi |
| if [ "$BROWSER_SHOULD_ENABLE" = "true" ]; then |
| PLUGIN_ALLOW_JSON=$(jq '. + ["browser"]' <<<"$PLUGIN_ALLOW_JSON") |
| fi |
| if [ -n "${TELEGRAM_BOT_TOKEN:-}" ]; then |
| PLUGIN_ALLOW_JSON=$(jq '. + ["telegram"]' <<<"$PLUGIN_ALLOW_JSON") |
| fi |
| if [ "$WHATSAPP_ENABLED_NORMALIZED" = "true" ]; then |
| PLUGIN_ALLOW_JSON=$(jq '. + ["whatsapp"]' <<<"$PLUGIN_ALLOW_JSON") |
| fi |
|
|
| |
| BROWSER_DISABLED=true |
| if [ "$BROWSER_SHOULD_ENABLE" = "true" ]; then BROWSER_DISABLED=false; fi |
|
|
| CONFIG_JSON=$(jq \ |
| --argjson allow "$PLUGIN_ALLOW_JSON" \ |
| --argjson browserDisabled "$BROWSER_DISABLED" \ |
| '.plugins.allow = $allow |
| | .plugins.deny = ["lmstudio","xai"] |
| | .plugins.entries.lmstudio.enabled = false |
| | .plugins.entries.xai.enabled = false |
| | del(.plugins.entries.acpx) |
| | (if $browserDisabled then |
| .plugins.entries.browser.enabled = false | .browser.enabled = false |
| else . end)' <<<"$CONFIG_JSON") |
|
|
| if [ "$BROWSER_SHOULD_ENABLE" = "true" ]; then |
| CONFIG_JSON=$(jq \ |
| '.browser = { |
| "enabled": true, |
| "defaultProfile": "openclaw", |
| "headless": true, |
| "noSandbox": true, |
| "extraArgs": [ |
| "--headless=new", |
| "--no-sandbox", |
| "--disable-setuid-sandbox", |
| "--no-zygote", |
| "--disable-dev-shm-usage", |
| "--disable-gpu", |
| "--remote-debugging-address=127.0.0.1", |
| "--disable-features=UseDBus,MediaRouter", |
| "--password-store=basic", |
| "--no-first-run", |
| "--disable-background-networking", |
| "--disable-sync", |
| "--disable-translate", |
| "--disable-notifications", |
| "--disable-speech-api" |
| ] |
| } |
| | .agents.defaults.sandbox.browser.allowHostControl = true' <<<"$CONFIG_JSON") |
| fi |
|
|
| |
| |
| |
| CONFIG_JSON=$(jq \ |
| --arg spaceHost "${SPACE_HOST:-}" \ |
| --arg password "${OPENCLAW_PASSWORD:-}" \ |
| '.gateway.controlUi.dangerouslyDisableDeviceAuth = true |
| | (if $spaceHost != "" then |
| .gateway.controlUi.allowedOrigins = ["https://" + $spaceHost] |
| else . end) |
| | (if ($password != "" and (.gateway.auth.token // "") == "") then |
| .gateway.auth.mode = "password" | .gateway.auth.password = $password |
| else . end)' <<<"$CONFIG_JSON") |
|
|
| |
| |
| |
| if [ -n "${TRUSTED_PROXIES:-}" ]; then |
| PROXIES_JSON=$(echo "$TRUSTED_PROXIES" | tr ',' '\n' | sed 's/^ *//;s/ *$//' | jq -R . | jq -s .) |
| CONFIG_JSON=$(echo "$CONFIG_JSON" | jq ".gateway.trustedProxies += $PROXIES_JSON | .gateway.trustedProxies |= unique") |
| fi |
|
|
| |
| |
| |
| if [ -n "${ALLOWED_ORIGINS:-}" ]; then |
| ORIGINS_JSON=$(echo "$ALLOWED_ORIGINS" | tr ',' '\n' | sed 's/^ *//;s/ *$//' | jq -R . | jq -s .) |
| CONFIG_JSON=$(echo "$CONFIG_JSON" | jq ".gateway.controlUi.allowedOrigins += $ORIGINS_JSON | .gateway.controlUi.allowedOrigins |= unique") |
| fi |
|
|
| resolve_telegram_api_root() { |
| local candidate="$(trim_var "${CLOUDFLARE_PROXY_URL:-}")" |
| if [ -n "$candidate" ]; then |
| case "$candidate" in |
| http://*|https://*) |
| printf '%s' "$candidate" |
| return 0 |
| ;; |
| *) |
| echo "Warning: invalid CLOUDFLARE_PROXY_URL '$candidate' (must start with http:// or https://); falling back to direct Telegram API." >&2 |
| ;; |
| esac |
| fi |
| printf '%s' "https://api.telegram.org" |
| } |
| TELEGRAM_API_ROOT="$(resolve_telegram_api_root)" |
|
|
|
|
| |
| if [ -n "${TELEGRAM_BOT_TOKEN:-}" ]; then |
| CONFIG_JSON=$(echo "$CONFIG_JSON" | jq '.plugins.entries.telegram = {"enabled": true}') |
| |
| CLEAN_TG_TOKEN=$(echo "$TELEGRAM_BOT_TOKEN" | tr -d '[:space:]') |
| export TELEGRAM_BOT_TOKEN="$CLEAN_TG_TOKEN" |
| |
| export OPENCLAW_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY=1 |
| export OPENCLAW_TELEGRAM_DNS_RESULT_ORDER=ipv4first |
| |
| export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--dns-result-order=ipv4first" |
| |
| CONFIG_JSON=$(echo "$CONFIG_JSON" | jq --arg token "$CLEAN_TG_TOKEN" --arg proxy_url "$TELEGRAM_API_ROOT" ' |
| .channels.telegram.enabled = true |
| | .channels.telegram.botToken = $token |
| | .channels.telegram.commands.native = false |
| | .channels.telegram.timeoutSeconds = 60 |
| | (if $proxy_url != "" then .channels.telegram.apiRoot = $proxy_url else . end) |
| | .channels.telegram.retry = { |
| "attempts": 5, |
| "minDelayMs": 800, |
| "maxDelayMs": 30000, |
| "jitter": 0.2 |
| } |
| ') |
| |
| if [ -n "${TELEGRAM_ALLOWED_USERS:-}" ]; then |
| |
| IDS_JSON=$(echo "$TELEGRAM_ALLOWED_USERS" | tr ',' '\n' | sed 's/^ *//;s/ *$//' | jq -R . | jq -s .) |
| CONFIG_JSON=$(jq \ |
| --argjson ids "$IDS_JSON" \ |
| '.channels.telegram += {"dmPolicy": "allowlist", "allowFrom": $ids}' <<<"$CONFIG_JSON") |
| elif [ -n "${TELEGRAM_USER_IDS:-}" ]; then |
| |
| IDS_JSON=$(echo "$TELEGRAM_USER_IDS" | tr ',' '\n' | sed 's/^ *//;s/ *$//' | jq -R . | jq -s .) |
| CONFIG_JSON=$(jq \ |
| --argjson ids "$IDS_JSON" \ |
| '.channels.telegram += {"dmPolicy": "allowlist", "allowFrom": $ids}' <<<"$CONFIG_JSON") |
| elif [ -n "${TELEGRAM_USER_ID:-}" ]; then |
| |
| CONFIG_JSON=$(jq \ |
| --arg userId "$TELEGRAM_USER_ID" \ |
| '.channels.telegram += {"dmPolicy": "allowlist", "allowFrom": [$userId]}' <<<"$CONFIG_JSON") |
| fi |
| fi |
|
|
| |
| if [ "$WHATSAPP_ENABLED_NORMALIZED" = "true" ]; then |
| CONFIG_JSON=$(echo "$CONFIG_JSON" | jq '.plugins.entries.whatsapp = {"enabled": true}') |
| CONFIG_JSON=$(echo "$CONFIG_JSON" | jq '.channels.whatsapp = {"dmPolicy": "pairing"}') |
| fi |
|
|
| |
| EXISTING_CONFIG="/home/node/.openclaw/openclaw.json" |
| WHATSAPP_CONFIG_ENABLED=false |
| if [ "$WHATSAPP_ENABLED_NORMALIZED" = "true" ]; then |
| WHATSAPP_CONFIG_ENABLED=true |
| fi |
| TELEGRAM_CONFIG_ENABLED=false |
| if [ -n "${TELEGRAM_BOT_TOKEN:-}" ]; then |
| TELEGRAM_CONFIG_ENABLED=true |
| fi |
| if [ -f "$EXISTING_CONFIG" ]; then |
| echo "Restored config found β patching required fields and runtime channel/plugin toggles..." |
| PATCHED=$(jq \ |
| --arg token "$GATEWAY_TOKEN" \ |
| --arg model "$LLM_MODEL" \ |
| --arg fileLevel "$OPENCLAW_FILE_LOG_LEVEL" \ |
| --arg consoleLevel "$OPENCLAW_CONSOLE_LOG_LEVEL" \ |
| --arg consoleStyle "$OPENCLAW_CONSOLE_LOG_STYLE" \ |
| --argjson desired "$CONFIG_JSON" \ |
| --argjson fileLogConfigured "$OPENCLAW_FILE_LOG_LEVEL_CONFIGURED" \ |
| --argjson consoleLogConfigured "$OPENCLAW_CONSOLE_LOG_LEVEL_CONFIGURED" \ |
| --argjson consoleStyleConfigured "$OPENCLAW_CONSOLE_LOG_STYLE_CONFIGURED" \ |
| --argjson whatsappConfigured "$WHATSAPP_ENABLED_CONFIGURED" \ |
| --argjson whatsappEnabled "$WHATSAPP_CONFIG_ENABLED" \ |
| --argjson telegramConfigured "$TELEGRAM_CONFIG_ENABLED" \ |
| '(.channels.whatsapp // {}) as $existingWhatsapp |
| | .gateway.auth.token = $token |
| | .agents.defaults.model = $model |
| | .gateway.port = ($desired.gateway.port // .gateway.port) |
| | if $fileLogConfigured then .logging.level = $fileLevel else . end |
| | if $consoleLogConfigured then .logging.consoleLevel = $consoleLevel else . end |
| | if $consoleStyleConfigured then .logging.consoleStyle = $consoleStyle else . end |
| | .channels = ((.channels // {}) * ($desired.channels // {})) |
| | .plugins.allow = (((.plugins.allow // []) + ($desired.plugins.allow // [])) | unique) |
| | .plugins.deny = (((.plugins.deny // []) + ($desired.plugins.deny // [])) | unique) |
| | .plugins.entries = ((.plugins.entries // {}) * ($desired.plugins.entries // {})) |
| | if $whatsappEnabled then |
| ($desired.channels.whatsapp // {"dmPolicy": "pairing"}) as $desiredWhatsapp |
| | .plugins.entries.whatsapp.enabled = true |
| | .channels.whatsapp = (($existingWhatsapp * $desiredWhatsapp) |
| | if ($existingWhatsapp | has("dmPolicy")) then .dmPolicy = $existingWhatsapp.dmPolicy else . end |
| | if ($existingWhatsapp | has("allowFrom")) then .allowFrom = $existingWhatsapp.allowFrom else . end) |
| elif $whatsappConfigured then |
| .plugins.entries.whatsapp.enabled = false |
| | del(.channels.whatsapp) |
| else |
| . |
| end |
| | if $telegramConfigured then |
| .channels.telegram = (($desired.channels.telegram // {}) * (.channels.telegram // {})) |
| | .channels.telegram.botToken = $desired.channels.telegram.botToken |
| else |
| del(.channels.telegram) |
| | .plugins.entries.telegram.enabled = false |
| end' \ |
| "$EXISTING_CONFIG" 2>/dev/null) |
|
|
| if [ -n "$PATCHED" ]; then |
| echo "$PATCHED" > "$EXISTING_CONFIG.tmp" \ |
| && mv "$EXISTING_CONFIG.tmp" "$EXISTING_CONFIG" |
| echo "Config patched successfully." |
| else |
| echo "Patch failed β writing fresh config." |
| echo "$CONFIG_JSON" > "$EXISTING_CONFIG" |
| fi |
| else |
| echo "No restored config β writing fresh config..." |
| echo "$CONFIG_JSON" > "$EXISTING_CONFIG" |
| fi |
| chmod 600 "$EXISTING_CONFIG" |
|
|
| |
| |
| export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require /home/node/app/iframe-fix.cjs --require /home/node/app/multi-provider-key-rotator.cjs" |
|
|
| |
| echo "" |
| echo "Version : ${OPENCLAW_DISPLAY_VERSION}" |
| echo "Model : ${LLM_MODEL}" |
| if [ -n "${TELEGRAM_BOT_TOKEN:-}" ]; then |
| echo "Telegram : enabled" |
| else |
| echo "Telegram : not configured" |
| fi |
| if [ "$WHATSAPP_ENABLED_NORMALIZED" = "true" ]; then |
| echo "WhatsApp : enabled" |
| else |
| echo "WhatsApp : disabled" |
| fi |
| if [ -n "${HF_TOKEN:-}" ]; then |
| echo "Backup : ${BACKUP_DATASET:-huggingclaw-backup} (every ${SYNC_INTERVAL:-180}s)" |
| else |
| echo "Backup : disabled" |
| fi |
| if [ -n "${CLOUDFLARE_PROXY_URL:-}" ]; then |
| echo "Proxy : ${CLOUDFLARE_PROXY_URL}" |
| fi |
| |
| |
| if hc_is_true "${HUGGINGCLAW_JUPYTER_ENABLED:-false}"; then |
| RUNTIME_JUPYTER_ENABLED=true |
| else |
| RUNTIME_JUPYTER_ENABLED="$DEV_MODE_ENABLED" |
| fi |
| |
| export PATH="$HOME/.local/bin:$PATH" |
|
|
| |
| if [ "$DEV_MODE_ENABLED" = "true" ] && ! python3 -c "import jupyterlab" >/dev/null 2>&1; then |
| echo "Terminal : installing JupyterLab..." |
| if python3 -m pip install -q --user --no-cache-dir --break-system-packages "jupyterlab>=4.2,<5" "tornado>=6.3" "ipywidgets>=8.1" >/dev/null 2>&1; then |
| echo "Terminal : installed" |
| python3 -c "from pathlib import Path; import shutil, jupyter_server; d=Path(jupyter_server.__file__).parent/'templates'; d.mkdir(parents=True,exist_ok=True); shutil.copyfile('/home/node/app/login.html', d/'login.html')" || true |
| else |
| echo "Terminal : install failed β disabling for this boot" |
| RUNTIME_JUPYTER_ENABLED=false |
| fi |
| fi |
| if [ "$RUNTIME_JUPYTER_ENABLED" = "true" ] && ! python3 -c "import jupyterlab" >/dev/null 2>&1; then |
| echo "WARNING: jupyter-lab still unavailable; disabling terminal for this boot." |
| RUNTIME_JUPYTER_ENABLED=false |
| fi |
| export HUGGINGCLAW_JUPYTER_ENABLED="$RUNTIME_JUPYTER_ENABLED" |
|
|
| if [ -n "${SPACE_HOST:-}" ]; then |
| if [ "$RUNTIME_JUPYTER_ENABLED" = "true" ]; then |
| echo "Routes : /app/ (Control UI), /terminal/ (JupyterLab)" |
| else |
| echo "Routes : /app/ (Control UI)" |
| fi |
| fi |
| echo "" |
|
|
| |
| if [ -n "${WEBHOOK_URL:-}" ]; then |
| WEBHOOK_BODY=$(jq -n \ |
| --arg model "$LLM_MODEL" \ |
| '{"event":"restart","status":"success","message":"HuggingClaw gateway has started/restarted.","model":$model}') |
| curl -s -X POST "$WEBHOOK_URL" \ |
| -H "Content-Type: application/json" \ |
| -d "$WEBHOOK_BODY" >/dev/null 2>&1 & |
| fi |
|
|
| |
| graceful_shutdown() { |
| echo "Shutting down..." |
| if [ -f "/home/node/app/openclaw-sync.py" ] && [ -n "${HF_TOKEN:-}" ]; then |
| echo "Saving state before exit..." |
| |
| |
| |
| |
| |
| |
| if [ -n "${SYNC_LOOP_PID:-}" ]; then |
| kill "$SYNC_LOOP_PID" 2>/dev/null || true |
| |
| sleep 0.5 |
| fi |
| timeout 8s python3 /home/node/app/openclaw-sync.py sync-once-settled || \ |
| echo "Warning: could not complete settled shutdown sync" |
| sleep 1 |
| python3 /home/node/app/openclaw-sync.py sync-once || \ |
| echo "Warning: could not complete final shutdown sync" |
| elif [ -f "/home/node/app/openclaw-sync.py" ]; then |
| echo "HF_TOKEN not set; skipping shutdown backup sync." |
| fi |
| kill $(jobs -p) 2>/dev/null |
| exit 0 |
| } |
| trap graceful_shutdown SIGTERM SIGINT |
|
|
| BROWSER_WARMED_UP=false |
| warmup_browser() { |
| [ "$BROWSER_SHOULD_ENABLE" = "true" ] || return 0 |
| |
| [ "$BROWSER_WARMED_UP" = "false" ] || return 0 |
| BROWSER_WARMED_UP=true |
|
|
| ( |
| sleep 8 |
|
|
| local attempt |
| for attempt in 1 2 3 4 5 6; do |
| if openclaw browser --browser-profile openclaw start >/dev/null 2>&1; then |
| openclaw browser --browser-profile openclaw open about:blank >/dev/null 2>&1 || true |
| echo "Managed browser ready." |
| return 0 |
| fi |
| sleep 5 |
| done |
|
|
| echo "Warning: managed browser warm-up did not complete; first browser action may need a retry." |
| ) & |
| } |
|
|
|
|
| |
| export LLM_MODEL="$LLM_MODEL" |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export HUGGINGFACE_HUB_TOKEN="${HUGGINGFACE_HUB_TOKEN:-${HF_TOKEN:-}}" |
|
|
| |
| node /home/node/app/health-server.js & |
| HEALTH_PID=$! |
|
|
| start_jupyter_once() { |
| [ "$RUNTIME_JUPYTER_ENABLED" = "true" ] || return 0 |
| if [ -n "${JUPYTER_PID:-}" ] && kill -0 "$JUPYTER_PID" 2>/dev/null; then |
| return 0 |
| fi |
|
|
| |
| |
| if { [ -z "${JUPYTER_TOKEN:-}" ] || [ "${JUPYTER_TOKEN}" = "huggingface" ]; } && [ -n "${GATEWAY_TOKEN:-}" ]; then |
| JUPYTER_TOKEN="$GATEWAY_TOKEN" |
| fi |
|
|
| |
| |
| if [ -z "${JUPYTER_TOKEN:-}" ] || [ "${JUPYTER_TOKEN}" = "huggingface" ]; then |
| echo "ERROR: JUPYTER_TOKEN is unset or still set to the insecure default (\"huggingface\")." >&2 |
| echo " JupyterLab grants full shell access. Set a strong, unique token in your Space secrets." >&2 |
| echo " Hint: openssl rand -hex 32" >&2 |
| echo " DEV_MODE active but JupyterLab will NOT start until JUPYTER_TOKEN is changed." >&2 |
| return 1 |
| fi |
| JUPYTER_ROOT_DIR="${JUPYTER_ROOT_DIR:-/home/node}" |
| if [ "$JUPYTER_ROOT_DIR" = "/home/node/.openclaw/workspace" ] && [ "$DEVDATA_ENABLED" = "true" ]; then |
| echo "Jupyter root was set to OpenClaw workspace; moving Jupyter root to /home/node/devdata to keep BACKUP and DEVDATA datasets separate." |
| JUPYTER_ROOT_DIR="/home/node/devdata" |
| fi |
| mkdir -p "$JUPYTER_ROOT_DIR" |
| export JUPYTER_ROOT_DIR |
| if [ "$JUPYTER_ROOT_DIR" != "/home/node/app" ]; then |
| if [ -L "$JUPYTER_ROOT_DIR/HuggingClaw" ] || [ ! -e "$JUPYTER_ROOT_DIR/HuggingClaw" ]; then |
| ln -sfn /home/node/app "$JUPYTER_ROOT_DIR/HuggingClaw" |
| fi |
| fi |
| if [ "$JUPYTER_ROOT_DIR" != "/home/node/.openclaw/workspace" ]; then |
| if [ -L "$JUPYTER_ROOT_DIR/HuggingClaw-Workspace" ] || [ ! -e "$JUPYTER_ROOT_DIR/HuggingClaw-Workspace" ]; then |
| ln -sfn /home/node/.openclaw/workspace "$JUPYTER_ROOT_DIR/HuggingClaw-Workspace" |
| fi |
| fi |
| if [ "$JUPYTER_ROOT_DIR" != "/home/node/.openclaw" ]; then |
| if [ -L "$JUPYTER_ROOT_DIR/OpenClaw-Home" ] || [ ! -e "$JUPYTER_ROOT_DIR/OpenClaw-Home" ]; then |
| ln -sfn /home/node/.openclaw "$JUPYTER_ROOT_DIR/OpenClaw-Home" |
| fi |
| fi |
|
|
| |
| mkdir -p "$JUPYTER_ROOT_DIR/.jupyter" |
|
|
| echo "Terminal : starting (root: $JUPYTER_ROOT_DIR)" |
| JUPYTER_LOG_FILE="/tmp/jupyterlab.log" |
| |
| |
| export PYTHONPATH="" |
| python3 -m jupyterlab \ |
| --ip 127.0.0.1 \ |
| --port 8888 \ |
| --no-browser \ |
| --IdentityProvider.token="$JUPYTER_TOKEN" \ |
| --ServerApp.base_url=/terminal/ \ |
| --ServerApp.terminals_enabled=True \ |
| --ServerApp.terminado_settings='{"shell_command":["/bin/bash","-i"]}' \ |
| --ServerApp.allow_origin='*' \ |
| --ServerApp.allow_remote_access=True \ |
| --ServerApp.trust_xheaders=True \ |
| --ServerApp.tornado_settings="{'headers': {'Content-Security-Policy': 'frame-ancestors *'}}" \ |
| --IdentityProvider.cookie_options="{'SameSite': 'None', 'Secure': True}" \ |
| --ServerApp.disable_check_xsrf=True \ |
| --LabApp.news_url=None \ |
| --LabApp.check_for_updates_class=jupyterlab.NeverCheckForUpdate \ |
| --ServerApp.log_level=WARN \ |
| --ServerApp.root_dir="$JUPYTER_ROOT_DIR" \ |
| >> "$JUPYTER_LOG_FILE" 2>&1 & |
| JUPYTER_PID=$! |
| export JUPYTER_PID |
| echo "Terminal : started (PID: $JUPYTER_PID)" |
| } |
|
|
| |
| |
| |
| |
| |
| if [ "$RUNTIME_JUPYTER_ENABLED" = "true" ] && \ |
| [ "$DEVDATA_ENABLED" = "true" ] && \ |
| [ -n "${HF_TOKEN:-}" ] && \ |
| [ -f "/home/node/app/jupyter-devdata-sync.py" ] && \ |
| [ "${DEVDATA_DATASET_NAME:-huggingclaw-devdata}" != "${BACKUP_DATASET_NAME:-huggingclaw-backup}" ]; then |
| echo "DevData : restoring workspace..." |
| python3 /home/node/app/jupyter-devdata-sync.py --restore 2>/dev/null || \ |
| echo "DevData : restore warning (non-fatal); continuing startup." |
| fi |
|
|
| |
| |
| |
| if [ "$DEV_MODE_ENABLED" = "true" ]; then |
| if ! python3 -c "import jsonschema" >/dev/null 2>&1; then |
| python3 -m pip install -q --force-reinstall --no-cache-dir --break-system-packages "jsonschema>=4.0" >/dev/null 2>&1 || true |
| fi |
| fi |
|
|
| |
| |
| if [ "$RUNTIME_JUPYTER_ENABLED" = "true" ]; then |
| start_jupyter_once |
| fi |
|
|
| if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ]; then |
| echo "Setting up Cloudflare KeepAlive monitor..." |
| python3 /home/node/app/cloudflare-keepalive-setup.py || true |
| fi |
|
|
| |
| |
| |
| if [ ! -f "$STARTUP_FILE" ]; then |
| touch "$STARTUP_FILE" |
| chmod +x "$STARTUP_FILE" |
| echo "Created workspace/startup.sh" |
| fi |
| cat > /home/node/.bashrc << 'BASHRC' |
| export PATH="/home/node/.local/bin:$PATH" |
| export NPM_CONFIG_PREFIX="${NPM_CONFIG_PREFIX:-/home/node/.local}" |
| export npm_config_prefix="$NPM_CONFIG_PREFIX" |
| export PYTHONUSERBASE="${PYTHONUSERBASE:-/home/node/.local}" |
| export DEBIAN_FRONTEND="${DEBIAN_FRONTEND:-noninteractive}" |
| if [ -z "${PS1:-}" ] || [ "$PS1" = "$ " ]; then |
| export PS1="\u@\h:\w\$ " |
| fi |
| STARTUP_FILE="/home/node/.openclaw/workspace/startup.sh" |
| _hc_append() { |
| if [ "${HUGGINGCLAW_CAPTURE_DISABLE:-0}" = "1" ]; then |
| return 0 |
| fi |
| local line="$*" |
| mkdir -p "$(dirname "$STARTUP_FILE")" |
| touch "$STARTUP_FILE" |
| chmod +x "$STARTUP_FILE" 2>/dev/null || true |
| grep -qxF "$line" "$STARTUP_FILE" 2>/dev/null || echo "$line" >> "$STARTUP_FILE" |
| } |
| _hc_quote_args() { |
| local quoted=() |
| local arg |
| for arg in "$@"; do |
| printf -v arg '%q' "$arg" |
| quoted+=("$arg") |
| done |
| printf '%s' "${quoted[*]}" |
| } |
| _hc_append_cmd() { |
| local cmd="$1" |
| shift |
| local args |
| args=$(_hc_quote_args "$@") |
| if [ -n "$args" ]; then |
| _hc_append "$cmd $args" |
| else |
| _hc_append "$cmd" |
| fi |
| } |
| _hc_args_without_flags() { |
| local out=() |
| local arg |
| for arg in "$@"; do |
| case "$arg" in |
| ''|-) ;; |
| --*) ;; |
| -*) ;; |
| *) out+=("$arg") ;; |
| esac |
| done |
| printf '%s\n' "${out[@]}" |
| } |
| _hc_has_install_targets() { |
| local item |
| while IFS= read -r item; do |
| [ -n "$item" ] && return 0 |
| done <<EOF |
| $(_hc_args_without_flags "$@") |
| EOF |
| return 1 |
| } |
| _hc_allow_openclaw_plugins() { |
| local config="/home/node/.openclaw/openclaw.json" |
| [ -f "$config" ] || return 0 |
|
|
| local plugins=() |
| local plugin |
| for plugin in "$@"; do |
| [ -n "$plugin" ] || continue |
| [[ "$plugin" == -* ]] && continue |
| plugins+=("$plugin") |
| if [[ "$plugin" == @openclaw/* ]]; then |
| plugins+=("${plugin#@openclaw/}") |
| fi |
| done |
| [ "${#plugins[@]}" -gt 0 ] || return 0 |
|
|
| local plugins_json |
| plugins_json=$(printf '%s\n' "${plugins[@]}" | jq -R 'select(length > 0)' | jq -s 'unique') || return 0 |
| jq --argjson plugins "$plugins_json" \ |
| '.plugins.allow = (((.plugins.allow // []) + $plugins) | unique)' \ |
| "$config" > "$config.tmp" && mv "$config.tmp" "$config" |
| } |
| _hc_has_arg() { |
| local needle="$1" |
| shift |
| local arg |
| for arg in "$@"; do |
| [ "$arg" = "$needle" ] && return 0 |
| done |
| return 1 |
| } |
| _hc_can_sudo_apt() { |
| command -v sudo >/dev/null 2>&1 && sudo -n apt-get --version >/dev/null 2>&1 |
| } |
| _hc_apt_install() { |
| if [ "$(id -u)" -eq 0 ]; then |
| command apt-get update && command apt-get install -y "$@" |
| elif _hc_can_sudo_apt; then |
| sudo apt-get update && sudo apt-get install -y "$@" |
| else |
| echo "Error: apt install needs root. Rebuild with the latest HuggingClaw image or add packages to Dockerfile." >&2 |
| return 1 |
| fi |
| } |
| apt-get() { |
| case "${1:-}" in |
| install) |
| shift |
| _hc_apt_install "$@" |
| local rc=$? |
| if [ $rc -eq 0 ]; then |
| _hc_has_install_targets "$@" && _hc_append_cmd "sudo apt-get update && sudo apt-get install -y" "$@" |
| fi |
| return $rc |
| ;; |
| update) |
| if [ "$(id -u)" -eq 0 ]; then |
| command apt-get "$@" |
| elif _hc_can_sudo_apt; then |
| sudo apt-get "$@" |
| else |
| command apt-get "$@" |
| fi |
| return $? |
| ;; |
| *) |
| command apt-get "$@" |
| return $? |
| ;; |
| esac |
| } |
| apt() { |
| case "${1:-}" in |
| install) |
| shift |
| _hc_apt_install "$@" |
| local rc=$? |
| if [ $rc -eq 0 ]; then |
| _hc_has_install_targets "$@" && _hc_append_cmd "sudo apt-get update && sudo apt-get install -y" "$@" |
| fi |
| return $rc |
| ;; |
| update) |
| if [ "$(id -u)" -eq 0 ]; then |
| command apt "$@" |
| elif _hc_can_sudo_apt; then |
| sudo apt "$@" |
| else |
| command apt "$@" |
| fi |
| return $? |
| ;; |
| *) |
| command apt "$@" |
| return $? |
| ;; |
| esac |
| } |
| pip() { |
| if [ "${1:-}" = "install" ] && [ -z "${VIRTUAL_ENV:-}" ] && ! _hc_has_arg --user "$@" && ! _hc_has_arg --prefix "$@"; then |
| command pip install --user --break-system-packages "${@:2}" |
| else |
| command pip "$@" |
| fi |
| local rc=$? |
| |
| if [ $rc -eq 0 ] && [ "${1:-}" = "install" ] \ |
| && ! _hc_has_arg -r "${@:2}" && ! _hc_has_arg --requirement "${@:2}" \ |
| && _hc_has_install_targets "${@:2}"; then |
| _hc_append_cmd "python3 -m pip install --user" "${@:2}" |
| fi |
| return $rc |
| } |
| pip3() { |
| if [ "${1:-}" = "install" ] && [ -z "${VIRTUAL_ENV:-}" ] && ! _hc_has_arg --user "$@" && ! _hc_has_arg --prefix "$@"; then |
| command pip3 install --user --break-system-packages "${@:2}" |
| else |
| command pip3 "$@" |
| fi |
| local rc=$? |
| if [ $rc -eq 0 ] && [ "${1:-}" = "install" ] \ |
| && ! _hc_has_arg -r "${@:2}" && ! _hc_has_arg --requirement "${@:2}" \ |
| && _hc_has_install_targets "${@:2}"; then |
| _hc_append_cmd "python3 -m pip install --user" "${@:2}" |
| fi |
| return $rc |
| } |
| python() { |
| if [ "${1:-}" = "-m" ] && [ "${2:-}" = "pip" ] && [ "${3:-}" = "install" ] && [ -z "${VIRTUAL_ENV:-}" ] && ! _hc_has_arg --user "${@:3}" && ! _hc_has_arg --prefix "${@:3}"; then |
| command python -m pip install --user --break-system-packages "${@:4}" |
| else |
| command python "$@" |
| fi |
| local rc=$? |
| if [ $rc -eq 0 ] && [ "${1:-}" = "-m" ] && [ "${2:-}" = "pip" ] && [ "${3:-}" = "install" ] \ |
| && ! _hc_has_arg -r "${@:4}" && ! _hc_has_arg --requirement "${@:4}" \ |
| && _hc_has_install_targets "${@:4}"; then |
| _hc_append_cmd "python3 -m pip install --user" "${@:4}" |
| fi |
| return $rc |
| } |
| python3() { |
| if [ "${1:-}" = "-m" ] && [ "${2:-}" = "pip" ] && [ "${3:-}" = "install" ] && [ -z "${VIRTUAL_ENV:-}" ] && ! _hc_has_arg --user "${@:3}" && ! _hc_has_arg --prefix "${@:3}"; then |
| command python3 -m pip install --user --break-system-packages "${@:4}" |
| else |
| command python3 "$@" |
| fi |
| local rc=$? |
| if [ $rc -eq 0 ] && [ "${1:-}" = "-m" ] && [ "${2:-}" = "pip" ] && [ "${3:-}" = "install" ] \ |
| && ! _hc_has_arg -r "${@:4}" && ! _hc_has_arg --requirement "${@:4}" \ |
| && _hc_has_install_targets "${@:4}"; then |
| _hc_append_cmd "python3 -m pip install --user" "${@:4}" |
| fi |
| return $rc |
| } |
| npm() { |
| command npm "$@" |
| local rc=$? |
| if [ $rc -eq 0 ] && { [ "${1:-}" = "install" ] || [ "${1:-}" = "i" ]; } && { [ "${2:-}" = "-g" ] || [ "${2:-}" = "--global" ]; } && _hc_has_install_targets "${@:3}"; then |
| _hc_append_cmd "npm install -g" "${@:3}" |
| fi |
| return $rc |
| } |
| openclaw() { |
| command openclaw "$@" |
| local rc=$? |
| if [ $rc -eq 0 ] && [ "${1:-}" = "plugins" ] && [ "${2:-}" = "install" ] && _hc_has_install_targets "${@:3}"; then |
| _hc_allow_openclaw_plugins "${@:3}" |
| _hc_append_cmd "openclaw plugins install" "${@:3}" |
| fi |
| return $rc |
| } |
| |
| uv() { |
| command uv "$@" |
| local rc=$? |
| |
| |
| if [ $rc -eq 0 ] && [ "${1:-}" = "pip" ] && [ "${2:-}" = "install" ] \ |
| && ! _hc_has_arg -r "${@:3}" && ! _hc_has_arg --requirements "${@:3}" \ |
| && _hc_has_install_targets "${@:3}"; then |
| _hc_append_cmd "uv pip install" "${@:3}" |
| fi |
| return $rc |
| } |
| |
| pipx() { |
| command pipx "$@" |
| local rc=$? |
| if [ $rc -eq 0 ] && [ "${1:-}" = "install" ] && _hc_has_install_targets "${@:2}"; then |
| _hc_append_cmd "pipx install" "${@:2}" |
| fi |
| return $rc |
| } |
| BASHRC |
| cat > /home/node/.profile <<'PROFILE' |
| [ -n "${BASH_VERSION:-}" ] && [ -f ~/.bashrc ] && . ~/.bashrc |
| PROFILE |
| echo "Shell capture wrappers ready." |
|
|
| |
| EXISTING_CONFIG="/home/node/.openclaw/openclaw.json" |
| if [ -f "$EXISTING_CONFIG" ]; then |
| INSTALLS=$(jq -r '.plugins.installs // {} | keys[]' "$EXISTING_CONFIG" 2>/dev/null || echo "") |
| if [ -n "$INSTALLS" ]; then |
| echo "Re-installing plugins from config..." |
| while IFS= read -r pkg; do |
| [ -z "$pkg" ] && continue |
| |
| if openclaw plugins install "$pkg" 2>/dev/null; then |
| echo " Installed: $pkg" |
| elif openclaw plugins install "@openclaw/$pkg" 2>/dev/null; then |
| echo " Installed: @openclaw/$pkg" |
| else |
| echo " Warning: could not install $pkg" |
| fi |
| done <<< "$INSTALLS" |
| echo "Plugins done." |
| fi |
| fi |
|
|
| |
| |
| |
| |
| |
| HC_STARTUP_STRICT_NORMALIZED=$(printf '%s' "${HUGGINGCLAW_STARTUP_STRICT:-false}" | tr '[:upper:]' '[:lower:]') |
| hc_run_startup_command() { |
| local source_label="$1" |
| local command_text="$2" |
| [ -n "$command_text" ] || return 0 |
|
|
| echo "[startup:${source_label}] $command_text" |
| set +e |
| HUGGINGCLAW_CAPTURE_DISABLE=1 bash -lc "$command_text" |
| local rc=$? |
| set -e |
| if [ "$rc" -eq 0 ]; then |
| echo "[startup:${source_label}] ok" |
| return 0 |
| fi |
|
|
| HC_STARTUP_FAILURES=$((HC_STARTUP_FAILURES + 1)) |
| echo "ERROR: startup command failed (${source_label}, exit ${rc}): $command_text" >&2 |
| return "$rc" |
| } |
|
|
| hc_run_startup_script() { |
| local source_label="$1" |
| local script_text="$2" |
| [ -n "$script_text" ] || return 0 |
|
|
| local script_file |
| script_file=$(mktemp "/tmp/huggingclaw-startup-${source_label//[^A-Za-z0-9_.-]/_}.XXXXXX.sh") |
| { |
| |
| |
| |
| echo 'export HUGGINGCLAW_CAPTURE_DISABLE=1' |
| echo '[ -f /home/node/.bashrc ] && . /home/node/.bashrc' |
| printf '%s\n' "$script_text" |
| } > "$script_file" |
| chmod 700 "$script_file" |
|
|
| echo "[startup:${source_label}] running script (${script_file})" |
| set +e |
| bash "$script_file" |
| local rc=$? |
| set -e |
| rm -f "$script_file" |
|
|
| if [ "$rc" -eq 0 ]; then |
| echo "[startup:${source_label}] ok" |
| return 0 |
| fi |
|
|
| HC_STARTUP_FAILURES=$((HC_STARTUP_FAILURES + 1)) |
| echo "ERROR: startup script failed (${source_label}, exit ${rc})" >&2 |
| return "$rc" |
| } |
| hc_run_startup_script_b64() { |
| local source_label="$1" |
| local encoded_script="$2" |
| [ -n "$encoded_script" ] || return 0 |
|
|
| local script_text |
| if ! script_text=$(printf '%s' "$encoded_script" | base64 -d 2>/dev/null); then |
| HC_STARTUP_FAILURES=$((HC_STARTUP_FAILURES + 1)) |
| echo "ERROR: startup script base64 decode failed (${source_label})" >&2 |
| return 1 |
| fi |
|
|
| hc_run_startup_script "$source_label" "$script_text" |
| } |
|
|
|
|
| hc_run_startup_auto() { |
| local source_label="$1" |
| local payload="$2" |
| [ -n "$payload" ] || return 0 |
|
|
| if [[ "$payload" == base64:* ]]; then |
| hc_run_startup_script_b64 "$source_label" "${payload#base64:}" |
| elif [[ "$payload" == b64:* ]]; then |
| hc_run_startup_script_b64 "$source_label" "${payload#b64:}" |
| else |
| hc_run_startup_script "$source_label" "$payload" |
| fi |
| } |
|
|
| hc_run_command_block() { |
| local source_label="$1" |
| local command_block="$2" |
| local line |
| local index=0 |
|
|
| while IFS= read -r line || [ -n "$line" ]; do |
| |
| [[ "$line" =~ ^[[:space:]]*$ ]] && continue |
| [[ "$line" =~ ^[[:space:]]* |
|
|
| index=$((index + 1)) |
| hc_run_startup_command "${source_label}[${index}]" "$line" || true |
| done <<< "$command_block" |
| } |
|
|
| sync_installed_plugins_into_allow() { |
| local config="/home/node/.openclaw/openclaw.json" |
| [ -f "$config" ] || return 0 |
|
|
| local patched |
| patched=$(jq ' |
| (.plugins.installs // {}) as $installs |
| | ($installs | keys) as $installed |
| | ($installed | map(if startswith("@openclaw/") then sub("^@openclaw/"; "") else . end)) as $short |
| | .plugins.allow = (((.plugins.allow // []) + $installed + $short) | unique) |
| ' "$config" 2>/dev/null) || { |
| echo "Warning: could not sync installed plugins into plugins.allow" |
| return 0 |
| } |
|
|
| echo "$patched" > "$config.tmp" && mv "$config.tmp" "$config" |
| } |
|
|
| hc_finish_startup_commands() { |
| if [ "$HC_STARTUP_FAILURES" -gt 0 ]; then |
| echo "ERROR: ${HC_STARTUP_FAILURES} startup command(s) failed. Check the log lines above." >&2 |
| if [ "$HC_STARTUP_STRICT_NORMALIZED" = "true" ] || [ "$HC_STARTUP_STRICT_NORMALIZED" = "1" ] || [ "$HC_STARTUP_STRICT_NORMALIZED" = "yes" ]; then |
| echo "ERROR: HUGGINGCLAW_STARTUP_STRICT=true, stopping startup." >&2 |
| exit 1 |
| fi |
| fi |
| return 0 |
| } |
|
|
| |
| |
| |
| if [ -n "${HUGGINGCLAW_APT_PACKAGES:-}" ]; then |
| echo "Installing apt packages from HUGGINGCLAW_APT_PACKAGES..." |
| read -r -a HC_APT_PACKAGES <<< "$HUGGINGCLAW_APT_PACKAGES" |
| if command -v sudo >/dev/null 2>&1; then |
| if sudo apt-get update && sudo apt-get install -y "${HC_APT_PACKAGES[@]}"; then |
| echo "HUGGINGCLAW_APT_PACKAGES install complete." |
| else |
| HC_STARTUP_FAILURES=$((HC_STARTUP_FAILURES + 1)) |
| echo "ERROR: HUGGINGCLAW_APT_PACKAGES install failed: ${HUGGINGCLAW_APT_PACKAGES}" >&2 |
| fi |
| else |
| HC_STARTUP_FAILURES=$((HC_STARTUP_FAILURES + 1)) |
| echo "ERROR: sudo is unavailable; HUGGINGCLAW_APT_PACKAGES install skipped" >&2 |
| fi |
| fi |
| if [ -n "${HUGGINGCLAW_PIP_PACKAGES:-}" ]; then |
| echo "Installing Python packages from HUGGINGCLAW_PIP_PACKAGES..." |
| read -r -a HC_PIP_PACKAGES <<< "$HUGGINGCLAW_PIP_PACKAGES" |
| if python3 -m pip install --user --break-system-packages "${HC_PIP_PACKAGES[@]}"; then |
| echo "HUGGINGCLAW_PIP_PACKAGES install complete." |
| else |
| HC_STARTUP_FAILURES=$((HC_STARTUP_FAILURES + 1)) |
| echo "ERROR: HUGGINGCLAW_PIP_PACKAGES install failed: ${HUGGINGCLAW_PIP_PACKAGES}" >&2 |
| fi |
| fi |
| if [ -n "${HUGGINGCLAW_NPM_PACKAGES:-}" ]; then |
| echo "Installing global npm packages from HUGGINGCLAW_NPM_PACKAGES..." |
| read -r -a HC_NPM_PACKAGES <<< "$HUGGINGCLAW_NPM_PACKAGES" |
| if npm install -g "${HC_NPM_PACKAGES[@]}"; then |
| echo "HUGGINGCLAW_NPM_PACKAGES install complete." |
| else |
| HC_STARTUP_FAILURES=$((HC_STARTUP_FAILURES + 1)) |
| echo "ERROR: HUGGINGCLAW_NPM_PACKAGES install failed: ${HUGGINGCLAW_NPM_PACKAGES}" >&2 |
| fi |
| fi |
| if [ -n "${HUGGINGCLAW_OPENCLAW_PLUGINS:-}" ]; then |
| echo "Installing OpenClaw plugins from HUGGINGCLAW_OPENCLAW_PLUGINS..." |
| read -r -a HC_OPENCLAW_PLUGINS <<< "$HUGGINGCLAW_OPENCLAW_PLUGINS" |
| if openclaw plugins install "${HC_OPENCLAW_PLUGINS[@]}"; then |
| echo "HUGGINGCLAW_OPENCLAW_PLUGINS install complete." |
| else |
| HC_STARTUP_FAILURES=$((HC_STARTUP_FAILURES + 1)) |
| echo "ERROR: HUGGINGCLAW_OPENCLAW_PLUGINS install failed: ${HUGGINGCLAW_OPENCLAW_PLUGINS}" >&2 |
| fi |
| fi |
|
|
| |
| if [ "${AUTO_DOCTOR:-false}" = "true" ]; then |
| openclaw doctor --fix || true |
| fi |
|
|
| |
| |
| |
| |
| if [ -n "${HUGGINGCLAW_RUN:-}" ]; then |
| hc_run_startup_auto "HUGGINGCLAW_RUN" "$HUGGINGCLAW_RUN" || true |
| fi |
| if [ -n "${HUGGINGCLAW_STARTUP_COMMANDS:-}" ]; then |
| echo "Running commands from HUGGINGCLAW_STARTUP_COMMANDS..." |
| hc_run_command_block "HUGGINGCLAW_STARTUP_COMMANDS" "$HUGGINGCLAW_STARTUP_COMMANDS" |
| fi |
| for HC_STARTUP_INDEX in $(seq 1 100); do |
| HC_STARTUP_VAR="HUGGINGCLAW_STARTUP_COMMAND_${HC_STARTUP_INDEX}" |
| if [ -n "${!HC_STARTUP_VAR:-}" ]; then |
| hc_run_startup_command "$HC_STARTUP_VAR" "${!HC_STARTUP_VAR}" || true |
| fi |
| done |
| if [ -n "${HUGGINGCLAW_STARTUP_SCRIPT:-}" ]; then |
| hc_run_startup_script "HUGGINGCLAW_STARTUP_SCRIPT" "$HUGGINGCLAW_STARTUP_SCRIPT" || true |
| fi |
| if [ -n "${HUGGINGCLAW_STARTUP_SCRIPT_B64:-}" ]; then |
| hc_run_startup_script_b64 "HUGGINGCLAW_STARTUP_SCRIPT_B64" "$HUGGINGCLAW_STARTUP_SCRIPT_B64" || true |
| fi |
| for HC_STARTUP_INDEX in $(seq 1 20); do |
| HC_STARTUP_VAR="HUGGINGCLAW_STARTUP_SCRIPT_B64_${HC_STARTUP_INDEX}" |
| if [ -n "${!HC_STARTUP_VAR:-}" ]; then |
| hc_run_startup_script_b64 "$HC_STARTUP_VAR" "${!HC_STARTUP_VAR}" || true |
| fi |
| done |
|
|
| |
| STARTUP_FILE="/home/node/.openclaw/workspace/startup.sh" |
| if [ ! -f "$STARTUP_FILE" ]; then |
| touch "$STARTUP_FILE" |
| chmod +x "$STARTUP_FILE" |
| echo "Created workspace/startup.sh" |
| fi |
| if [ -s "$STARTUP_FILE" ]; then |
| echo "Running workspace/startup.sh script..." |
| hc_run_startup_script "workspace/startup.sh" "$(cat "$STARTUP_FILE")" || true |
| echo "Workspace startup script complete." |
| fi |
| hc_finish_startup_commands |
| sync_installed_plugins_into_allow |
|
|
| |
| GATEWAY_RESTART_DELAY="${GATEWAY_RESTART_DELAY:-2}" |
| GATEWAY_MAX_RESTARTS="${GATEWAY_MAX_RESTARTS:-0}" |
| GATEWAY_RESTART_COUNT=0 |
| SYNC_LOOP_PID="" |
| GUARDIAN_PID="" |
|
|
| sync_before_gateway_restart() { |
| [ -n "${HF_TOKEN:-}" ] || return 0 |
| [ -f "/home/node/app/openclaw-sync.py" ] || return 0 |
|
|
| echo "Gateway stopped; saving latest OpenClaw state before restart..." |
| python3 /home/node/app/openclaw-sync.py sync-once-settled || \ |
| echo "Warning: could not sync settled state before gateway restart" |
| } |
|
|
| start_background_devdata_sync() { |
| if [ "$DEV_MODE_ENABLED" != "true" ]; then |
| return 0 |
| fi |
| if [ "$DEVDATA_ENABLED" != "true" ]; then |
| echo "DevData : disabled by DEVDATA=${DEVDATA_RAW}" |
| return 0 |
| fi |
| if [ -z "${HF_TOKEN:-}" ]; then |
| echo "DevData : disabled (HF_TOKEN missing)" |
| return 0 |
| fi |
| if [ "${DEVDATA_DATASET_NAME:-huggingclaw-devdata}" = "${BACKUP_DATASET_NAME:-huggingclaw-backup}" ]; then |
| echo "DevData : disabled (DEVDATA_DATASET_NAME must be separate from BACKUP_DATASET_NAME)" |
| return 0 |
| fi |
| if [ ! -f "/home/node/app/jupyter-devdata-sync.py" ]; then |
| echo "DevData : script missing; skipped" |
| return 0 |
| fi |
| |
| |
| |
| |
| if [ -n "${DEVDATA_SYNC_PID:-}" ] && kill -0 "$DEVDATA_SYNC_PID" 2>/dev/null; then |
| return 0 |
| fi |
| echo "DevData : enabled (dataset=${DEVDATA_DATASET_NAME:-huggingclaw-devdata})" |
| python3 -u /home/node/app/jupyter-devdata-sync.py >> /tmp/devdata-sync.log 2>&1 & |
| DEVDATA_SYNC_PID=$! |
| } |
|
|
| start_background_sync_once() { |
| [ -n "${HF_TOKEN:-}" ] || return 0 |
|
|
| if [ -n "$SYNC_LOOP_PID" ] && kill -0 "$SYNC_LOOP_PID" 2>/dev/null; then |
| return 0 |
| fi |
|
|
| python3 -u /home/node/app/openclaw-sync.py loop >> /tmp/workspace-sync.log 2>&1 & |
| SYNC_LOOP_PID=$! |
| } |
|
|
| start_guardian_once() { |
| [ "$WHATSAPP_ENABLED_NORMALIZED" = "true" ] || return 0 |
|
|
| if [ -n "$GUARDIAN_PID" ] && kill -0 "$GUARDIAN_PID" 2>/dev/null; then |
| return 0 |
| fi |
|
|
| node /home/node/app/wa-guardian.js & |
| GUARDIAN_PID=$! |
| echo "WhatsApp Guardian started (PID: $GUARDIAN_PID)" |
| } |
|
|
| |
| if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then |
| if command -v dbus-launch >/dev/null 2>&1; then |
| eval "$(dbus-launch --sh-syntax 2>/dev/null)" || true |
| export DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS:-disabled:}" |
| else |
| export DBUS_SESSION_BUS_ADDRESS="disabled:" |
| fi |
| fi |
|
|
| while true; do |
| |
| if [ -n "${HEALTH_PID:-}" ] && ! kill -0 "$HEALTH_PID" 2>/dev/null; then |
| echo "Warning: health-server exited (PID $HEALTH_PID dead); restarting..." |
| node /home/node/app/health-server.js & |
| HEALTH_PID=$! |
| echo "Health server restarted (PID: $HEALTH_PID)" |
| fi |
|
|
| |
| if [ "$RUNTIME_JUPYTER_ENABLED" = "true" ]; then |
| if [ -n "${JUPYTER_PID:-}" ]; then |
| if ! kill -0 "$JUPYTER_PID" 2>/dev/null; then |
| echo "Warning: JupyterLab exited (PID $JUPYTER_PID dead); checking log..." |
| tail -5 /tmp/jupyterlab.log 2>/dev/null || echo "No log file" |
| echo "Attempting JupyterLab restart..." |
| unset JUPYTER_PID |
| start_jupyter_once |
| fi |
| else |
| |
| start_jupyter_once |
| fi |
| fi |
|
|
| if [ "${AUTO_DOCTOR:-false}" = "true" ]; then |
| openclaw doctor --fix || true |
| fi |
| echo "Launching OpenClaw gateway on port ${GATEWAY_PORT}..." |
|
|
| GATEWAY_ARGS=(gateway run --port "${GATEWAY_PORT}" --bind lan) |
| if [ "${GATEWAY_VERBOSE:-0}" = "1" ]; then |
| GATEWAY_ARGS+=(--verbose) |
| echo "Gateway verbose logging enabled (GATEWAY_VERBOSE=1)" |
| fi |
|
|
| |
| |
| |
| |
| stdbuf -oL -eL openclaw "${GATEWAY_ARGS[@]}" 2>&1 | tee -a /home/node/.openclaw/gateway.log & |
| GATEWAY_PID=$! |
|
|
| |
| |
| |
| GATEWAY_READY_TIMEOUT="${GATEWAY_READY_TIMEOUT:-90}" |
| ready=false |
| for ((i=0; i<GATEWAY_READY_TIMEOUT; i++)); do |
| if (echo > /dev/tcp/127.0.0.1/${GATEWAY_PORT}) 2>/dev/null; then |
| ready=true |
| break |
| fi |
| if ! kill -0 "$GATEWAY_PID" 2>/dev/null; then |
| break |
| fi |
| sleep 1 |
| done |
|
|
| if [ "$ready" != "true" ]; then |
| echo "" |
| echo "Gateway failed to start. Last 30 lines of log:" |
| echo "ββββββββββββββββββββββββββββββββββββββββββββ" |
| tail -30 /home/node/.openclaw/gateway.log |
| if [ "$DEV_MODE_ENABLED" = "true" ]; then |
| echo "Gateway failed β DEV_MODE active, retrying in 10s..." |
| sleep 10 |
| continue |
| else |
| echo "Gateway failed β exiting." |
| exit 1 |
| fi |
| fi |
|
|
| |
| start_guardian_once |
|
|
| |
| warmup_browser |
|
|
| |
| |
| |
| start_background_sync_once |
| start_background_devdata_sync |
|
|
| set +e |
| wait "$GATEWAY_PID" |
| GATEWAY_EXIT_CODE=$? |
| set -e |
|
|
| sync_before_gateway_restart |
|
|
| GATEWAY_RESTART_COUNT=$((GATEWAY_RESTART_COUNT + 1)) |
| if [ "$GATEWAY_MAX_RESTARTS" != "0" ] && [ "$GATEWAY_RESTART_COUNT" -ge "$GATEWAY_MAX_RESTARTS" ]; then |
| echo "Gateway exited with code ${GATEWAY_EXIT_CODE}; restart limit (${GATEWAY_MAX_RESTARTS}) reached." |
| echo "Gateway stopped β JupyterLab and env-builder still running." |
| break |
| fi |
|
|
| echo "Gateway exited with code ${GATEWAY_EXIT_CODE}; restarting in ${GATEWAY_RESTART_DELAY}s..." |
| sleep "$GATEWAY_RESTART_DELAY" |
| done |
|
|