| #!/bin/bash |
| set -e |
|
|
| |
| |
| |
|
|
| |
| OPENCLAW_VERSION="${OPENCLAW_VERSION:-latest}" |
| WHATSAPP_ENABLED="${WHATSAPP_ENABLED:-false}" |
| WHATSAPP_ENABLED_NORMALIZED=$(printf '%s' "$WHATSAPP_ENABLED" | tr '[:upper:]' '[:lower:]') |
| SYNC_INTERVAL="${SYNC_INTERVAL:-180}" |
| echo "" |
| echo " ββββββββββββββββββββββββββββββββββββββββββββ" |
| echo " β π¦ HuggingClaw Gateway β" |
| 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 [[ "$LLM_MODEL" == "anthropic/gemini"* ]]; then |
| LLM_MODEL=$(echo "$LLM_MODEL" | sed 's/^anthropic\//google\//') |
| echo "β οΈ 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|mistralai) 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" ;; |
| |
| *) |
| export ANTHROPIC_API_KEY="$LLM_API_KEY" |
| ;; |
| esac |
|
|
| |
| 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 |
| chmod 700 /home/node/.openclaw |
| chmod 700 /home/node/.openclaw/credentials |
|
|
| |
| if [ -n "$HF_TOKEN" ]; then |
| echo "π Validating HF token..." |
| HF_AUTH_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $HF_TOKEN" https://huggingface.co/api/repos/create --max-time 10 2>/dev/null || echo "000") |
| if [ "$HF_AUTH_STATUS" = "401" ]; then |
| echo " β οΈ HF token is invalid or expired! Workspace backup will not work." |
| echo " Get a new token: https://huggingface.co/settings/tokens" |
| else |
| echo " β
HF token is valid" |
| fi |
| fi |
|
|
| |
| if [ -n "$HF_USERNAME" ] && [ -n "$HF_TOKEN" ]; then |
| BACKUP_DATASET="${BACKUP_DATASET_NAME:-huggingclaw-backup}" |
| BACKUP_URL="https://${HF_USERNAME}:${HF_TOKEN}@huggingface.co/datasets/${HF_USERNAME}/${BACKUP_DATASET}" |
| |
| |
| echo "π¦ Checking HF Dataset: ${HF_USERNAME}/${BACKUP_DATASET}..." |
| DATASET_CHECK=$(curl -s -o /dev/null -w "%{http_code}" \ |
| -H "Authorization: Bearer $HF_TOKEN" \ |
| "https://huggingface.co/api/datasets/${HF_USERNAME}/${BACKUP_DATASET}" \ |
| --max-time 10 2>/dev/null || echo "000") |
| |
| if [ "$DATASET_CHECK" = "404" ]; then |
| echo " π Dataset not found, creating ${HF_USERNAME}/${BACKUP_DATASET}..." |
| CREATE_RESULT=$(curl -s -w "\n%{http_code}" \ |
| -X POST "https://huggingface.co/api/repos/create" \ |
| -H "Authorization: Bearer $HF_TOKEN" \ |
| -H "Content-Type: application/json" \ |
| -d "{\"type\":\"dataset\",\"name\":\"${BACKUP_DATASET}\",\"private\":true}" \ |
| --max-time 15 2>/dev/null || echo "error") |
| CREATE_STATUS=$(echo "$CREATE_RESULT" | tail -1) |
| if [ "$CREATE_STATUS" = "200" ] || [ "$CREATE_STATUS" = "201" ]; then |
| echo " β
Dataset created: ${HF_USERNAME}/${BACKUP_DATASET} (private)" |
| else |
| echo " β οΈ Could not create dataset (HTTP $CREATE_STATUS). Create it manually:" |
| echo " https://huggingface.co/datasets/create" |
| fi |
| elif [ "$DATASET_CHECK" = "200" ]; then |
| echo " β
Dataset exists" |
| else |
| echo " β οΈ Could not check dataset (HTTP $DATASET_CHECK)" |
| fi |
| |
| |
| echo "π¦ Restoring workspace..." |
| WORKSPACE="/home/node/.openclaw/workspace" |
| GIT_USER_EMAIL="${WORKSPACE_GIT_USER:-openclaw@example.com}" |
| GIT_USER_NAME="${WORKSPACE_GIT_NAME:-OpenClaw Bot}" |
| |
| cd "$WORKSPACE" |
| if [ ! -d ".git" ]; then |
| git init -q |
| git remote add origin "$BACKUP_URL" |
| else |
| git remote set-url origin "$BACKUP_URL" |
| fi |
| |
| git config user.email "$GIT_USER_EMAIL" |
| git config user.name "$GIT_USER_NAME" |
| |
| if git fetch origin main 2>/dev/null; then |
| git reset --hard origin/main 2>/dev/null && echo " β
Workspace restored!" |
| else |
| echo " β οΈ No remote data yet, starting fresh." |
| fi |
| cd / |
| fi |
|
|
| |
| STATE_BACKUP_ROOT="/home/node/.openclaw/workspace/.huggingclaw-state/openclaw" |
| if [ -d "$STATE_BACKUP_ROOT" ]; then |
| echo "π§ Restoring OpenClaw state..." |
| for source_path in "$STATE_BACKUP_ROOT"/*; do |
| [ -e "$source_path" ] || continue |
| name="$(basename "$source_path")" |
| target_path="/home/node/.openclaw/${name}" |
|
|
| rm -rf "$target_path" |
| mkdir -p "$(dirname "$target_path")" |
| cp -R "$source_path" "$target_path" |
| done |
| echo " β
OpenClaw state restored" |
| fi |
|
|
| |
| WA_BACKUP_DIR="/home/node/.openclaw/workspace/.huggingclaw-state/credentials/whatsapp/default" |
| WA_CREDS_DIR="/home/node/.openclaw/credentials/whatsapp/default" |
| if [ "$WHATSAPP_ENABLED_NORMALIZED" = "true" ] && [ -d "$WA_BACKUP_DIR" ]; then |
| WA_FILE_COUNT=$(find "$WA_BACKUP_DIR" -type f | wc -l | tr -d ' ') |
| if [ "$WA_FILE_COUNT" -ge 2 ]; then |
| echo "π± Restoring WhatsApp credentials..." |
| rm -rf "$WA_CREDS_DIR" |
| mkdir -p "$(dirname "$WA_CREDS_DIR")" |
| cp -R "$WA_BACKUP_DIR" "$WA_CREDS_DIR" |
| chmod -R go-rwx /home/node/.openclaw/credentials/whatsapp 2>/dev/null || true |
| echo " β
WhatsApp credentials restored" |
| else |
| echo " β οΈ Saved WhatsApp credentials look incomplete (${WA_FILE_COUNT} files), skipping restore." |
| fi |
| fi |
|
|
| |
| CONFIG_JSON=$(cat <<'CONFIGEOF' |
| { |
| "gateway": { |
| "mode": "local", |
| "port": 7860, |
| "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": {} |
| } |
| } |
| CONFIGEOF |
| ) |
|
|
| |
| CONFIG_JSON=$(echo "$CONFIG_JSON" | jq ".gateway.auth.token = \"$GATEWAY_TOKEN\"") |
|
|
| |
| CONFIG_JSON=$(echo "$CONFIG_JSON" | jq ".agents.defaults.model = \"$LLM_MODEL\"") |
|
|
| |
| BROWSER_EXECUTABLE_PATH="" |
| for candidate in /usr/bin/chromium /usr/bin/chromium-browser /snap/bin/chromium; do |
| if [ -x "$candidate" ]; then |
| BROWSER_EXECUTABLE_PATH="$candidate" |
| break |
| fi |
| done |
|
|
| BROWSER_SHOULD_ENABLE=false |
| if [ -n "$BROWSER_EXECUTABLE_PATH" ] && [ -x "$BROWSER_EXECUTABLE_PATH" ]; then |
| BROWSER_SHOULD_ENABLE=true |
| fi |
|
|
| if [ "$BROWSER_SHOULD_ENABLE" = "true" ]; then |
| CONFIG_JSON=$(echo "$CONFIG_JSON" | jq \ |
| ".browser = { |
| \"enabled\": true, |
| \"defaultProfile\": \"openclaw\", |
| \"headless\": true, |
| \"noSandbox\": true, |
| \"executablePath\": \"$BROWSER_EXECUTABLE_PATH\" |
| } | .agents.defaults.sandbox.browser.allowHostControl = true") |
| fi |
|
|
| |
| if [ -n "$SPACE_HOST" ]; then |
| CONFIG_JSON=$(echo "$CONFIG_JSON" | jq ".gateway.controlUi.allowedOrigins = [\"https://${SPACE_HOST}\"]") |
| fi |
|
|
| |
| CONFIG_JSON=$(echo "$CONFIG_JSON" | jq ".gateway.controlUi.dangerouslyDisableDeviceAuth = true") |
|
|
| |
| if [ -n "$OPENCLAW_PASSWORD" ]; then |
| CONFIG_JSON=$(echo "$CONFIG_JSON" | jq ".gateway.auth.mode = \"password\" | .gateway.auth.password = \"$OPENCLAW_PASSWORD\"") |
| fi |
|
|
| |
| |
| |
| 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") |
| fi |
|
|
| |
| if [ -n "$TELEGRAM_BOT_TOKEN" ]; then |
| CONFIG_JSON=$(echo "$CONFIG_JSON" | jq '.plugins.entries.telegram = {"enabled": true}') |
| export TELEGRAM_BOT_TOKEN="$TELEGRAM_BOT_TOKEN" |
| |
| if [ -n "$TELEGRAM_USER_IDS" ]; then |
| |
| IDS_JSON=$(echo "$TELEGRAM_USER_IDS" | tr ',' '\n' | sed 's/^ *//;s/ *$//' | jq -R . | jq -s .) |
| CONFIG_JSON=$(echo "$CONFIG_JSON" | jq ".channels.telegram = {\"dmPolicy\": \"allowlist\", \"allowFrom\": $IDS_JSON}") |
| elif [ -n "$TELEGRAM_USER_ID" ]; then |
| |
| CONFIG_JSON=$(echo "$CONFIG_JSON" | jq ".channels.telegram = {\"dmPolicy\": \"allowlist\", \"allowFrom\": [\"$TELEGRAM_USER_ID\"]}") |
| 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 |
|
|
| |
| echo "$CONFIG_JSON" > "/home/node/.openclaw/openclaw.json" |
| chmod 600 /home/node/.openclaw/openclaw.json |
|
|
| |
| |
| export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require /home/node/app/iframe-fix.cjs" |
|
|
| |
| |
| |
| |
| patch_openclaw_scope_bug() { |
| local roots=( |
| "/home/node/.openclaw/openclaw-app" |
| "/usr/local/lib/node_modules/openclaw" |
| ) |
| local target="" |
| local updated=0 |
|
|
| for root in "${roots[@]}"; do |
| [ -d "$root/dist" ] || continue |
| target=$(find "$root/dist" -maxdepth 1 -type f -name 'gateway-cli-*.js' | head -n 1) |
| [ -n "$target" ] || continue |
|
|
| if grep -q 'return params.decision.kind !== "allow" || !params.controlUiAuthPolicy.allowBypass' "$target"; then |
| perl -0pi -e 's@return params\.decision\.kind !== "allow" \|\| !params\.controlUiAuthPolicy\.allowBypass && !params\.preserveInsecureLocalControlUiScopes && \(params\.authMethod === "token" \|\| params\.authMethod === "password" \|\| params\.authMethod === "trusted-proxy" \|\| params\.trustedProxyAuthOk === true\);@return params.decision.kind !== "allow";@g' "$target" |
|
|
| if grep -q 'return params.decision.kind !== "allow";' "$target"; then |
| echo "π§ Patched OpenClaw scope-clearing bug in $(basename "$target")" |
| updated=1 |
| break |
| fi |
| fi |
| done |
|
|
| if [ "$updated" -eq 0 ]; then |
| echo "β οΈ OpenClaw scope patch not applied (bundle format may have changed)" |
| fi |
| } |
|
|
| patch_openclaw_scope_bug |
|
|
| |
| echo "" |
| echo " ββββββββββββββββββββββββββββββββββββββββββββ" |
| echo " β π Configuration Summary β" |
| echo " ββββββββββββββββββββββββββββββββββββββββββββ€" |
| printf " β %-40s β\n" "OpenClaw: $OPENCLAW_VERSION" |
| printf " β %-40s β\n" "Model: $LLM_MODEL" |
| if [ -n "$TELEGRAM_BOT_TOKEN" ]; then |
| printf " β %-40s β\n" "Telegram: β
enabled" |
| else |
| printf " β %-40s β\n" "Telegram: β not configured" |
| fi |
| if [ "$WHATSAPP_ENABLED_NORMALIZED" = "true" ]; then |
| printf " β %-40s β\n" "WhatsApp: β
enabled" |
| else |
| printf " β %-40s β\n" "WhatsApp: β disabled" |
| fi |
| if [ "$BROWSER_SHOULD_ENABLE" = "true" ]; then |
| printf " β %-40s β\n" "Browser: β
${BROWSER_EXECUTABLE_PATH}" |
| else |
| printf " β %-40s β\n" "Browser: β unavailable" |
| fi |
| if [ -n "$HF_USERNAME" ] && [ -n "$HF_TOKEN" ]; then |
| printf " β %-40s β\n" "Backup: β
${HF_USERNAME}/${BACKUP_DATASET:-huggingclaw-backup}" |
| else |
| printf " β %-40s β\n" "Backup: β not configured" |
| fi |
| if [ -n "$OPENCLAW_PASSWORD" ]; then |
| printf " β %-40s β\n" "Auth: π password" |
| else |
| printf " β %-40s β\n" "Auth: π token" |
| fi |
| if [ -n "$SPACE_HOST" ]; then |
| printf " β %-40s β\n" "Control UI: https://${SPACE_HOST}/app" |
| printf " β %-40s β\n" "Dashboard: https://${SPACE_HOST}" |
| fi |
| SYNC_STATUS="β disabled" |
| if [ -n "$HF_USERNAME" ] && [ -n "$HF_TOKEN" ]; then |
| SYNC_STATUS="β
every ${SYNC_INTERVAL:-180}s" |
| fi |
| printf " β %-40s β\n" "Auto-sync: $SYNC_STATUS" |
| if [ -n "$WEBHOOK_URL" ]; then |
| printf " β %-40s β\n" "Webhooks: β
enabled" |
| fi |
| echo " ββββββββββββββββββββββββββββββββββββββββββββ" |
| echo "" |
|
|
| |
| if [ -n "$WEBHOOK_URL" ]; then |
| echo "π Sending restart webhook..." |
| curl -s -X POST "$WEBHOOK_URL" \ |
| -H "Content-Type: application/json" \ |
| -d '{"event":"restart", "status":"success", "message":"HuggingClaw gateway has started/restarted.", "model": "'"$LLM_MODEL"'"}' >/dev/null 2>&1 & |
| fi |
|
|
| |
| graceful_shutdown() { |
| echo "" |
| echo "π Shutting down gracefully..." |
|
|
| if [ -f "/home/node/app/workspace-sync.py" ]; then |
| echo "πΎ Saving OpenClaw state before exit..." |
| python3 /home/node/app/workspace-sync.py --sync-once || \ |
| echo " β οΈ Could not complete shutdown sync" |
| fi |
| |
| |
| kill $(jobs -p) 2>/dev/null |
| echo "π Goodbye!" |
| exit 0 |
| } |
| trap graceful_shutdown SIGTERM SIGINT |
|
|
| warmup_browser() { |
| [ "$BROWSER_SHOULD_ENABLE" = "true" ] || return 0 |
|
|
| ( |
| sleep 5 |
|
|
| local attempt |
| for attempt in 1 2 3 4 5; 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 2 |
| done |
|
|
| echo " β οΈ Managed browser warm-up did not complete; first browser action may need a retry" |
| ) & |
| } |
|
|
| |
| export LLM_MODEL="$LLM_MODEL" |
| |
| node /home/node/app/health-server.js & |
| HEALTH_PID=$! |
|
|
| |
| echo "π Launching OpenClaw gateway on port 7860..." |
| echo "" |
|
|
| GATEWAY_ARGS=(gateway run --port 7860 --bind lan) |
| if [ "${GATEWAY_VERBOSE:-0}" = "1" ]; then |
| GATEWAY_ARGS+=(--verbose) |
| echo "π Gateway verbose logging enabled (GATEWAY_VERBOSE=1)" |
| fi |
|
|
| openclaw "${GATEWAY_ARGS[@]}" 2>&1 | tee -a /home/node/.openclaw/gateway.log & |
| GATEWAY_PID=$! |
|
|
| |
| sleep 3 |
| if ! kill -0 $GATEWAY_PID 2>/dev/null; then |
| echo "" |
| echo "β Gateway failed to start. Last 30 lines of log:" |
| echo "ββββββββββββββββββββββββββββββββββββββββββββ" |
| tail -30 /home/node/.openclaw/gateway.log |
| exit 1 |
| fi |
|
|
| |
| if [ "$WHATSAPP_ENABLED_NORMALIZED" = "true" ]; then |
| node /home/node/app/wa-guardian.js & |
| GUARDIAN_PID=$! |
| echo "π‘οΈ WhatsApp Guardian started (PID: $GUARDIAN_PID)" |
| fi |
|
|
| |
| warmup_browser |
|
|
| |
| python3 -u /home/node/app/workspace-sync.py & |
|
|
| |
| wait $GATEWAY_PID |
|
|