#!/usr/bin/env bash set -euo pipefail # Preserve HF Space variables (from os.environ) before sourcing env files # Priority: HF Space vars > env files _HF_SPACE_VARS=( "OPENCLAW_BACKUP_DATASET_REPO" "OPENCLAW_RESTORE_DATASET_REPO" "OPENCLAW_BACKUP_ENABLED" "OPENCLAW_BACKUP_NPM_ENABLED" "OPENCLAW_RESTORE_NPM_ENABLED" "OPENCLAW_VERSION" "HF_TOKEN" "HF_STORAGE_REPO" ) declare -A _PRESERVED_HF_VARS for _var in "${_HF_SPACE_VARS[@]}"; do if [[ -n "${!_var:-}" ]]; then _PRESERVED_HF_VARS["$_var"]="${!_var}" fi done # Load environment from save-env.sh if available if [[ -f /etc/profile.d/openclaw-env.sh ]]; then # shellcheck source=/dev/null source /etc/profile.d/openclaw-env.sh fi # Restore HF Space variables (they take priority over env files) for _var in "${!_PRESERVED_HF_VARS[@]}"; do export "$_var"="${_PRESERVED_HF_VARS[$_var]}" done unset _HF_SPACE_VARS _PRESERVED_HF_VARS _var OPENCLAW_USER="${OPENCLAW_USER:-root}" OPENCLAW_GROUP="${OPENCLAW_GROUP:-root}" OPENCLAW_HOME="${OPENCLAW_HOME:-/root}" OPENCLAW_STATE_DIR="${OPENCLAW_STATE_DIR:-/root/.openclaw}" OPENCLAW_CONFIG_PATH="${OPENCLAW_CONFIG_PATH:-${OPENCLAW_STATE_DIR}/openclaw.json}" OPENCLAW_WORKSPACE_DIR="${OPENCLAW_WORKSPACE_DIR:-${OPENCLAW_STATE_DIR}/workspace}" OPENCLAW_GATEWAY_BIND="${OPENCLAW_GATEWAY_BIND:-lan}" OPENCLAW_GATEWAY_PORT="${OPENCLAW_GATEWAY_PORT:-18789}" OPENCLAW_INIT_GATEWAY_MODE="${OPENCLAW_INIT_GATEWAY_MODE:-local}" OPENCLAW_GATEWAY_AUTH_MODE="${OPENCLAW_GATEWAY_AUTH_MODE:-token}" OPENCLAW_LLM_PROVIDER="${OPENCLAW_LLM_PROVIDER:-thirdparty}" OPENCLAW_LLM_MODEL="${OPENCLAW_LLM_MODEL:-}" OPENCLAW_LLM_API="${OPENCLAW_LLM_API:-openai-completions}" OPENCLAW_LLM_BASE_URL_ENV="${OPENCLAW_LLM_BASE_URL_ENV:-OPENCLAW_LLM_BASE_URL}" OPENCLAW_LLM_API_KEY_ENV="${OPENCLAW_LLM_API_KEY_ENV:-OPENCLAW_LLM_API_KEY}" OPENCLAW_BACKUP_ENABLED="${OPENCLAW_BACKUP_ENABLED:-false}" OPENCLAW_BACKUP_NPM_ENABLED="${OPENCLAW_BACKUP_NPM_ENABLED:-true}" OPENCLAW_RESTORE_NPM_ENABLED="${OPENCLAW_RESTORE_NPM_ENABLED:-true}" OPENCLAW_BACKUP_CRON="${OPENCLAW_BACKUP_CRON:-*/12 * * * *}" OPENCLAW_BACKUP_SOURCE_DIR="${OPENCLAW_BACKUP_SOURCE_DIR:-${OPENCLAW_STATE_DIR}}" OPENCLAW_BACKUP_ROOT_CONFIG_DIR="${OPENCLAW_BACKUP_ROOT_CONFIG_DIR:-/root/.config}" OPENCLAW_BACKUP_ROOT_CODEX_DIR="${OPENCLAW_BACKUP_ROOT_CODEX_DIR:-/root/.codex}" OPENCLAW_BACKUP_ROOT_CLAUDE_DIR="${OPENCLAW_BACKUP_ROOT_CLAUDE_DIR:-/root/.claude}" OPENCLAW_BACKUP_ROOT_AGENTS_DIR="${OPENCLAW_BACKUP_ROOT_AGENTS_DIR:-/root/.agents}" OPENCLAW_BACKUP_ROOT_SSH_DIR="${OPENCLAW_BACKUP_ROOT_SSH_DIR:-/root/.ssh}" OPENCLAW_BACKUP_ROOT_ENV_DIR="${OPENCLAW_BACKUP_ROOT_ENV_DIR:-/root/.env.d}" OPENCLAW_BACKUP_ROOT_NPM_DIR="${OPENCLAW_BACKUP_ROOT_NPM_DIR:-/root/.npm}" OPENCLAW_BACKUP_ROOT_LARK_CLI_DIR="${OPENCLAW_BACKUP_ROOT_LARK_CLI_DIR:-/root/.lark-cli}" OPENCLAW_BACKUP_ENV_FILE_PATH="${OPENCLAW_BACKUP_ENV_FILE_PATH:-/root/.env.d/openclaw-backup.env}" OPENCLAW_STDOUT_LOG_PATH="${OPENCLAW_STDOUT_LOG_PATH:-/var/log/openclaw/gateway.stdout.log}" OPENCLAW_STDERR_LOG_PATH="${OPENCLAW_STDERR_LOG_PATH:-/var/log/openclaw/gateway.stderr.log}" OPENCLAW_SSHX_AUTO_START="${OPENCLAW_SSHX_AUTO_START:-false}" OPENCLAW_GATEWAY_CONTROLUI_ALLOW_INSECURE_AUTH="${OPENCLAW_GATEWAY_CONTROLUI_ALLOW_INSECURE_AUTH:-false}" OPENCLAW_GATEWAY_CONTROLUI_DANGEROUSLY_DISABLE_DEVICE_AUTH="${OPENCLAW_GATEWAY_CONTROLUI_DANGEROUSLY_DISABLE_DEVICE_AUTH:-false}" OPENCLAW_CHILD_PID="" OPENCLAW_SSHX_PID="" OPENCLAW_NEED_CONFIG_UPDATE=0 export HOME="$OPENCLAW_HOME" export OPENCLAW_HOME export OPENCLAW_STATE_DIR export OPENCLAW_CONFIG_PATH export OPENCLAW_BACKUP_ENV_FILE_PATH OPENCLAW_STEP_INDEX=0 OPENCLAW_ENTRYPOINT_TAG="openclaw-entrypoint" # External script dependencies OPENCLAW_REQUIRED_SCRIPTS=( "/usr/local/bin/openclaw-restore.sh" "/usr/local/bin/openclaw-backup-cron.sh" ) timestamp_utc() { date -u +"%Y-%m-%dT%H:%M:%SZ" } log_info() { printf '[%s] [INFO] %s: %s\n' "$(timestamp_utc)" "$OPENCLAW_ENTRYPOINT_TAG" "$*" } log_warn() { printf '[%s] [WARN] %s: %s\n' "$(timestamp_utc)" "$OPENCLAW_ENTRYPOINT_TAG" "$*" >&2 } log_error() { printf '[%s] [ERROR] %s: %s\n' "$(timestamp_utc)" "$OPENCLAW_ENTRYPOINT_TAG" "$*" >&2 } log_debug() { if is_true "${OPENCLAW_DEBUG:-false}"; then printf '[%s] [DEBUG] %s: %s\n' "$(timestamp_utc)" "$OPENCLAW_ENTRYPOINT_TAG" "$*" >&2 fi } run_step() { local description="$1" shift OPENCLAW_STEP_INDEX=$((OPENCLAW_STEP_INDEX + 1)) log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" log_info "STEP ${OPENCLAW_STEP_INDEX}: ${description}" log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" local step_start_time step_start_time=$(date +%s) if "$@"; then local step_end_time step_end_time=$(date +%s) local step_duration=$((step_end_time - step_start_time)) log_info "✓ STEP ${OPENCLAW_STEP_INDEX} COMPLETED: ${description} (${step_duration}s)" return 0 else local step_exit_code=$? log_error "✗ STEP ${OPENCLAW_STEP_INDEX} FAILED: ${description} (exit code: $step_exit_code)" return $step_exit_code fi } is_true() { local value value="$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')" [[ "$value" == "1" || "$value" == "true" || "$value" == "yes" || "$value" == "on" ]] } # Pre-flight checks for external dependencies preflight_checks() { log_info "=== Running Pre-flight Checks ===" local all_passed=true local missing_scripts=() # Check 1: External scripts log_info "Checking external script dependencies..." for script in "${OPENCLAW_REQUIRED_SCRIPTS[@]}"; do if [[ -f "$script" && -x "$script" ]]; then log_debug " ✓ Found: $script" elif [[ -f "$script" ]]; then log_warn " ⚠ Found but not executable: $script" missing_scripts+=("$script (not executable)") all_passed=false else log_warn " ✗ Missing: $script" missing_scripts+=("$script (not found)") all_passed=false fi done # Check 2: Required commands log_info "Checking required commands..." local required_commands=("python3" "openclaw") for cmd in "${required_commands[@]}"; do if command -v "$cmd" >/dev/null 2>&1; then log_debug " ✓ Found: $cmd" else log_error " ✗ Missing required command: $cmd" all_passed=false fi done # Check 3: Optional commands (with warnings) log_info "Checking optional commands..." local optional_commands=("gosu" "sshx" "openssl") for cmd in "${optional_commands[@]}"; do if command -v "$cmd" >/dev/null 2>&1; then log_debug " ✓ Found: $cmd" else log_warn " ⚠ Optional command not found: $cmd" fi done # Check 4: Environment validation log_info "Checking environment configuration..." if [[ -n "${OPENCLAW_BACKUP_DATASET_REPO:-}" ]]; then log_info " ✓ Backup dataset configured: $OPENCLAW_BACKUP_DATASET_REPO" else log_info " ℹ Backup dataset not configured (optional)" fi if [[ -n "${OPENCLAW_LLM_MODEL:-}" ]]; then log_info " ✓ LLM model configured: $OPENCLAW_LLM_MODEL" else log_info " ℹ LLM model not configured (optional)" fi # Summary if is_true "$all_passed"; then log_info "=== Pre-flight Checks Passed ===" return 0 else log_error "=== Pre-flight Checks Failed ===" if [[ ${#missing_scripts[@]} -gt 0 ]]; then log_error "Missing scripts:" printf '%s\n' "${missing_scripts[@]}" | while read -r line; do log_error " - $line" done fi return 1 fi } generate_gateway_token() { if command -v openssl >/dev/null 2>&1; then openssl rand -hex 32 else python3 - <<'PY' import secrets print(secrets.token_hex(32)) PY fi } mkdir_state_dirs() { mkdir -p "$OPENCLAW_STATE_DIR" "$OPENCLAW_WORKSPACE_DIR" mkdir -p "$OPENCLAW_STATE_DIR/identity" mkdir -p "$OPENCLAW_STATE_DIR/agents/main/agent" mkdir -p "$OPENCLAW_STATE_DIR/agents/main/sessions" mkdir -p "$OPENCLAW_STATE_DIR/logs" mkdir -p "$(dirname "$OPENCLAW_STDOUT_LOG_PATH")" "$(dirname "$OPENCLAW_STDERR_LOG_PATH")" touch "$OPENCLAW_STDOUT_LOG_PATH" "$OPENCLAW_STDERR_LOG_PATH" } fix_permissions() { local path if [[ "$(id -u)" -ne 0 ]]; then return 0 fi if [[ -e "$OPENCLAW_HOME" ]]; then chown "$OPENCLAW_USER:$OPENCLAW_GROUP" "$OPENCLAW_HOME" fi for path in \ "$OPENCLAW_STATE_DIR" \ "$OPENCLAW_WORKSPACE_DIR" \ "$OPENCLAW_STDOUT_LOG_PATH" \ "$OPENCLAW_STDERR_LOG_PATH" \ "$(dirname "$OPENCLAW_STDOUT_LOG_PATH")" \ "$(dirname "$OPENCLAW_STDERR_LOG_PATH")"; do if [[ -e "$path" ]]; then chown -R "$OPENCLAW_USER:$OPENCLAW_GROUP" "$path" fi done } write_or_update_config() { local existing_token existing_token="" if [[ -f "$OPENCLAW_CONFIG_PATH" ]]; then existing_token="$(python3 - <<'PY' import json import os config_path = os.environ["OPENCLAW_CONFIG_PATH"] try: with open(config_path, "r", encoding="utf-8") as fh: cfg = json.load(fh) token = cfg.get("gateway", {}).get("auth", {}).get("token", "") if isinstance(token, str): print(token.strip()) except Exception: pass PY )" fi if [[ -z "${OPENCLAW_GATEWAY_TOKEN:-}" ]]; then if [[ -n "$existing_token" ]]; then OPENCLAW_GATEWAY_TOKEN="$existing_token" else OPENCLAW_GATEWAY_TOKEN="$(generate_gateway_token)" fi fi export OPENCLAW_GATEWAY_TOKEN OPENCLAW_GATEWAY_PORT="$OPENCLAW_GATEWAY_PORT" \ OPENCLAW_GATEWAY_BIND="$OPENCLAW_GATEWAY_BIND" \ OPENCLAW_INIT_GATEWAY_MODE="$OPENCLAW_INIT_GATEWAY_MODE" \ OPENCLAW_CONFIG_PATH="$OPENCLAW_CONFIG_PATH" \ OPENCLAW_GATEWAY_TOKEN="$OPENCLAW_GATEWAY_TOKEN" \ OPENCLAW_GATEWAY_PASSWORD="${OPENCLAW_GATEWAY_PASSWORD:-}" \ OPENCLAW_GATEWAY_AUTH_MODE="$OPENCLAW_GATEWAY_AUTH_MODE" \ OPENCLAW_LLM_PROVIDER="$OPENCLAW_LLM_PROVIDER" \ OPENCLAW_LLM_MODEL="$OPENCLAW_LLM_MODEL" \ OPENCLAW_LLM_API="$OPENCLAW_LLM_API" \ OPENCLAW_LLM_BASE_URL_ENV="$OPENCLAW_LLM_BASE_URL_ENV" \ OPENCLAW_LLM_API_KEY_ENV="$OPENCLAW_LLM_API_KEY_ENV" \ OPENCLAW_GATEWAY_CONTROLUI_DANGEROUSLY_ALLOW_HOST_HEADER_ORIGIN_FALLBACK="${OPENCLAW_GATEWAY_CONTROLUI_DANGEROUSLY_ALLOW_HOST_HEADER_ORIGIN_FALLBACK:-true}" \ OPENCLAW_GATEWAY_CONTROLUI_ALLOW_INSECURE_AUTH="$OPENCLAW_GATEWAY_CONTROLUI_ALLOW_INSECURE_AUTH" \ OPENCLAW_GATEWAY_CONTROLUI_DANGEROUSLY_DISABLE_DEVICE_AUTH="$OPENCLAW_GATEWAY_CONTROLUI_DANGEROUSLY_DISABLE_DEVICE_AUTH" \ OPENCLAW_INIT_CONTROL_UI_ALLOWED_ORIGINS="${OPENCLAW_INIT_CONTROL_UI_ALLOWED_ORIGINS:-}" \ python3 - <<'PY' import json import os import sys from pathlib import Path config_path = Path(os.environ["OPENCLAW_CONFIG_PATH"]) config = {} if config_path.exists(): try: config = json.loads(config_path.read_text(encoding="utf-8")) except Exception: config = {} provider = os.environ.get("OPENCLAW_LLM_PROVIDER", "thirdparty").strip() or "thirdparty" model = (os.environ.get("OPENCLAW_LLM_MODEL") or "").strip() api = os.environ.get("OPENCLAW_LLM_API", "openai-completions").strip() or "openai-completions" base_env_name = os.environ.get("OPENCLAW_LLM_BASE_URL_ENV", "OPENCLAW_LLM_BASE_URL").strip() or "OPENCLAW_LLM_BASE_URL" key_env_name = os.environ.get("OPENCLAW_LLM_API_KEY_ENV", "OPENCLAW_LLM_API_KEY").strip() or "OPENCLAW_LLM_API_KEY" base_url_value = (os.environ.get(base_env_name) or "").strip() api_key_value = (os.environ.get(key_env_name) or "").strip() missing_llm_env = [] if not model: missing_llm_env.append("OPENCLAW_LLM_MODEL") if not base_url_value: missing_llm_env.append(base_env_name) if not api_key_value: missing_llm_env.append(key_env_name) custom_llm_ready = not missing_llm_env model_ref = f"{provider}/{model}" if custom_llm_ready else "" if missing_llm_env: print( "openclaw: skip custom LLM model configuration because missing: " + ", ".join(missing_llm_env), file=sys.stderr, ) sshx_auto_start = (os.environ.get("OPENCLAW_SSHX_AUTO_START") or "false").strip().lower() in { "1", "true", "yes", "on", } if not sshx_auto_start: print( "openclaw: if you want to configure LLM via sshx, consider setting OPENCLAW_SSHX_AUTO_START=true", file=sys.stderr, ) fallback_allowed_origins = [f"http://127.0.0.1:{os.environ.get('OPENCLAW_GATEWAY_PORT', '18789')}"] allowed_origins = fallback_allowed_origins raw_allowed_origins = (os.environ.get("OPENCLAW_INIT_CONTROL_UI_ALLOWED_ORIGINS") or "").strip() if raw_allowed_origins: try: parsed = json.loads(raw_allowed_origins) if isinstance(parsed, list): normalized = [] for value in parsed: if isinstance(value, str): item = value.strip() if item: normalized.append(item) if normalized: allowed_origins = normalized except Exception: pass fallback_origin = (os.environ.get("OPENCLAW_GATEWAY_CONTROLUI_DANGEROUSLY_ALLOW_HOST_HEADER_ORIGIN_FALLBACK") or "true").lower() in { "1", "true", "yes", "on", } allow_insecure_auth = (os.environ.get("OPENCLAW_GATEWAY_CONTROLUI_ALLOW_INSECURE_AUTH") or "false").lower() in { "1", "true", "yes", "on", } disable_device_auth = (os.environ.get("OPENCLAW_GATEWAY_CONTROLUI_DANGEROUSLY_DISABLE_DEVICE_AUTH") or "false").lower() in { "1", "true", "yes", "on", } gateway = config.get("gateway") if not isinstance(gateway, dict): gateway = {} gateway["mode"] = os.environ.get("OPENCLAW_INIT_GATEWAY_MODE", "local") gateway["bind"] = os.environ.get("OPENCLAW_GATEWAY_BIND", "lan") auth = gateway.get("auth") if not isinstance(auth, dict): auth = {} token_value = (os.environ.get("OPENCLAW_GATEWAY_TOKEN") or "").strip() password_value = (os.environ.get("OPENCLAW_GATEWAY_PASSWORD") or "").strip() if token_value: auth["token"] = token_value if password_value: auth["password"] = password_value valid_auth_modes = {"none", "token", "password", "trusted-proxy"} mode_env = (os.environ.get("OPENCLAW_GATEWAY_AUTH_MODE") or "").strip().lower() existing_mode = auth.get("mode") if mode_env in valid_auth_modes: auth["mode"] = mode_env elif password_value and not token_value: auth["mode"] = "password" elif token_value and not password_value: auth["mode"] = "token" elif token_value and password_value: auth["mode"] = "token" elif isinstance(existing_mode, str) and existing_mode.strip().lower() in valid_auth_modes: auth["mode"] = existing_mode.strip().lower() else: auth["mode"] = "token" gateway["auth"] = auth gateway.setdefault("controlUi", {}) gateway["controlUi"]["allowedOrigins"] = allowed_origins gateway["controlUi"]["dangerouslyAllowHostHeaderOriginFallback"] = fallback_origin gateway["controlUi"]["allowInsecureAuth"] = allow_insecure_auth gateway["controlUi"]["dangerouslyDisableDeviceAuth"] = disable_device_auth config["gateway"] = gateway agents = config.get("agents") if not isinstance(agents, dict): agents = {} defaults = agents.get("defaults") if not isinstance(defaults, dict): defaults = {} if custom_llm_ready: defaults["model"] = {"primary": model_ref} allow_models = defaults.get("models") if not isinstance(allow_models, dict): allow_models = {} allow_models.setdefault(model_ref, {"alias": "Default"}) defaults["models"] = allow_models agents["defaults"] = defaults config["agents"] = agents if custom_llm_ready: models_cfg = config.get("models") if not isinstance(models_cfg, dict): models_cfg = {} models_cfg["mode"] = "merge" providers = models_cfg.get("providers") if not isinstance(providers, dict): providers = {} provider_cfg = providers.get(provider) if not isinstance(provider_cfg, dict): provider_cfg = {} provider_cfg["baseUrl"] = f"${{{base_env_name}}}" provider_cfg["apiKey"] = f"${{{key_env_name}}}" provider_cfg["api"] = api provider_cfg["models"] = [{"id": model, "name": model}] providers[provider] = provider_cfg models_cfg["providers"] = providers config["models"] = models_cfg config_path.parent.mkdir(parents=True, exist_ok=True) config_path.write_text(json.dumps(config, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") os.chmod(config_path, 0o600) PY } write_backup_env_file() { local backup_env_file local keys=( # Backup core config OPENCLAW_BACKUP_DATASET_REPO OPENCLAW_BACKUP_REPO_TYPE OPENCLAW_BACKUP_PATH_PREFIX OPENCLAW_BACKUP_ENABLED OPENCLAW_BACKUP_CRON OPENCLAW_BACKUP_SOURCE_DIR OPENCLAW_BACKUP_WORK_DIR # Multi-dataset backup/restore support OPENCLAW_RESTORE_DATASET_REPO OPENCLAW_BACKUP_NPM_ENABLED OPENCLAW_RESTORE_NPM_ENABLED # Backup encryption (enabled flag only, password from env) OPENCLAW_BACKUP_ENCRYPTION_ENABLED # Note: Container restart/rebuild always triggers restore # Backup root directories OPENCLAW_BACKUP_ROOT_CONFIG_DIR OPENCLAW_BACKUP_ROOT_CODEX_DIR OPENCLAW_BACKUP_ROOT_CLAUDE_DIR OPENCLAW_BACKUP_ROOT_AGENTS_DIR OPENCLAW_BACKUP_ROOT_SSH_DIR OPENCLAW_BACKUP_ROOT_ENV_DIR OPENCLAW_BACKUP_ROOT_NPM_DIR OPENCLAW_BACKUP_ROOT_LARK_CLI_DIR # State and workspace OPENCLAW_STATE_DIR OPENCLAW_HOME OPENCLAW_WORKSPACE_DIR OPENCLAW_CONFIG_PATH # Incremental backup OPENCLAW_INCREMENTAL_BACKUP OPENCLAW_INCREMENTAL_INTERVAL_MINUTES # Dynamic backup OPENCLAW_DYNAMIC_BACKUP OPENCLAW_DYNAMIC_SMALL_THRESHOLD_MB OPENCLAW_DYNAMIC_MEDIUM_THRESHOLD_MB OPENCLAW_DYNAMIC_HIGH_CHANGE_RATE OPENCLAW_DYNAMIC_LOW_CHANGE_RATE OPENCLAW_DYNAMIC_MIN_CHANGED_FILES OPENCLAW_DYNAMIC_MIN_CHANGED_SIZE_KB # Full backup strategy OPENCLAW_FULL_BACKUP_INTERVAL_HOURS OPENCLAW_MAX_INCREMENTAL_BACKUPS # Backup retention and compression OPENCLAW_BACKUP_KEEP_COUNT OPENCLAW_BACKUP_COMPRESSION_LEVEL OPENCLAW_BACKUP_SPLIT_SIZE OPENCLAW_BACKUP_SIZE_WARNING_MB OPENCLAW_BACKUP_PRIVATE # Health check OPENCLAW_BACKUP_HEALTH_CHECK_ENABLED OPENCLAW_BACKUP_HEALTH_CHECK_BEFORE OPENCLAW_BACKUP_HEALTH_CHECK_AFTER OPENCLAW_BACKUP_MAX_RETRIES # Watchdog WATCHDOG_INTERVAL MAX_BACKUP_AGE_MINUTES FORCE_BACKUP_INTERVAL # Restore OPENCLAW_RESTORE_TIMEOUT # Extra OPENCLAW_BACKUP_EXTRA_DIRS OPENCLAW_BACKUP_EXTRA_FILES ) backup_env_file="${OPENCLAW_BACKUP_ENV_FILE_PATH:-/root/.env.d/openclaw-backup.env}" if [[ "$(id -u)" -ne 0 && "$backup_env_file" == "/root/.env.d/openclaw-backup.env" ]]; then backup_env_file="${OPENCLAW_STATE_DIR}/openclaw-backup.env" fi mkdir -p "$(dirname "$backup_env_file")" if ! : > "$backup_env_file"; then echo "openclaw: cannot write backup env file at $backup_env_file" >&2 return 1 fi for key in "${keys[@]}"; do local value="${!key:-}" if [[ -n "$value" ]]; then printf '%s=%q\n' "$key" "$value" >> "$backup_env_file" fi done OPENCLAW_BACKUP_ENV_FILE_PATH="$backup_env_file" export OPENCLAW_BACKUP_ENV_FILE_PATH if [[ "$(id -u)" -eq 0 ]]; then chown "$OPENCLAW_USER:$OPENCLAW_GROUP" "$backup_env_file" chmod 640 "$backup_env_file" else chmod 600 "$backup_env_file" fi } setup_ssh_agent_autostart() { # Check if SSH agent auto-start is enabled if ! is_true "${OPENCLAW_SSH_AGENT_AUTOSTART:-false}"; then log_info "SSH agent auto-start is disabled; skipping setup" return 0 fi log_info "Setting up SSH agent auto-start..." # Ensure the autostart script exists local autostart_script="/usr/local/bin/ssh-agent-autostart.sh" if [[ ! -f "$autostart_script" ]]; then log_warn "SSH agent autostart script not found at $autostart_script" return 0 fi # Make sure the script is executable chmod +x "$autostart_script" # Add SSH hook to .bashrc if not already present local bashrc_file="$OPENCLAW_HOME/.bashrc" local ssh_hook_marker="# OPENCLAW_SSH_AGENT_AUTOSTART" if [[ -f "$bashrc_file" ]]; then if grep -q "$ssh_hook_marker" "$bashrc_file" 2>/dev/null; then log_info "SSH agent hook already present in .bashrc" else log_info "Adding SSH agent hook to .bashrc..." # Decode and append the hook if [[ -n "${OPENCLAW_SSH_BASHRC_HOOK:-}" ]]; then echo "" >> "$bashrc_file" echo "$ssh_hook_marker" >> "$bashrc_file" echo "$OPENCLAW_SSH_BASHRC_HOOK" | base64 -d >> "$bashrc_file" echo "$ssh_hook_marker" >> "$bashrc_file" log_info "SSH agent hook added to .bashrc" else # Fallback: add default hook echo "" >> "$bashrc_file" echo "$ssh_hook_marker" >> "$bashrc_file" echo '# Auto-start SSH agent and load keys' >> "$bashrc_file" echo 'if [ -f /usr/local/bin/ssh-agent-autostart.sh ]; then' >> "$bashrc_file" echo ' source /usr/local/bin/ssh-agent-autostart.sh' >> "$bashrc_file" echo 'fi' >> "$bashrc_file" echo "$ssh_hook_marker" >> "$bashrc_file" log_info "Default SSH agent hook added to .bashrc" fi fi else log_warn ".bashrc not found at $bashrc_file" fi # Ensure /root/.ssh directory exists with correct permissions local ssh_dir="$OPENCLAW_HOME/.ssh" if [[ ! -d "$ssh_dir" ]]; then log_info "Creating $ssh_dir directory..." mkdir -p "$ssh_dir" chmod 700 "$ssh_dir" chown "$OPENCLAW_USER:$OPENCLAW_GROUP" "$ssh_dir" fi log_info "SSH agent auto-start setup complete" log_info "Place your SSH private keys in $ssh_dir and they will be automatically loaded" } setup_backup_cron() { if [[ "$(id -u)" -ne 0 ]]; then echo "openclaw: backup cron requires root; skip cron setup" >&2 return 0 fi if ! is_true "$OPENCLAW_BACKUP_ENABLED"; then return 0 fi if [[ -z "${OPENCLAW_BACKUP_DATASET_REPO:-}" ]]; then echo "openclaw: backup enabled but OPENCLAW_BACKUP_DATASET_REPO is empty; skip cron" >&2 return 0 fi if ! write_backup_env_file; then echo "openclaw: backup env file unavailable; skip cron setup" >&2 return 0 fi cat > /etc/cron.d/openclaw-backup <> /var/log/openclaw/backup.log 2>&1 EOFCRON chmod 0644 /etc/cron.d/openclaw-backup touch /var/log/openclaw/backup.log chown "$OPENCLAW_USER:$OPENCLAW_GROUP" /var/log/openclaw/backup.log # 创建恢复日志文件 touch /var/log/openclaw/restore.log chown "$OPENCLAW_USER:$OPENCLAW_GROUP" /var/log/openclaw/restore.log echo "openclaw: backup cron job registered (cron daemon is managed separately)" } restore_from_backup_on_startup() { log_info "=== Starting Backup Restore ===" if [[ -z "${OPENCLAW_BACKUP_DATASET_REPO:-}" ]]; then log_info "Backup restore skipped: OPENCLAW_BACKUP_DATASET_REPO is not configured" return 0 fi # 多数据集支持:显示恢复目标 local restore_dataset="${OPENCLAW_RESTORE_DATASET_REPO:-$OPENCLAW_BACKUP_DATASET_REPO}" log_info "Restore source: $restore_dataset" log_info "Backup target: $OPENCLAW_BACKUP_DATASET_REPO" log_info "Note: Restore is always performed on container restart/rebuild" if ! write_backup_env_file; then log_error "Failed to write backup environment file" log_warn "Backup restore skipped due to configuration error" return 0 fi log_debug "Backup environment file written: $OPENCLAW_BACKUP_ENV_FILE_PATH" local restore_start_time restore_start_time=$(date +%s) if /usr/local/bin/openclaw-restore.sh; then local restore_end_time restore_end_time=$(date +%s) local restore_duration=$((restore_end_time - restore_start_time)) log_info "✓ Backup restore completed successfully from dataset: ${restore_dataset} (${restore_duration}s)" if [[ -f "/tmp/openclaw-restore-skipped-no-backup" ]]; then log_info "Restore skipped due to no existing backup (new dataset), will generate new config" rm -f /tmp/openclaw-restore-skipped-no-backup OPENCLAW_NEED_CONFIG_UPDATE=1 fi else local restore_exit_code=$? local restore_end_time restore_end_time=$(date +%s) local restore_duration=$((restore_end_time - restore_start_time)) if [[ "$restore_exit_code" -eq 2 ]]; then log_info "Backup restore skipped: no dataset configured (${restore_duration}s)" else log_warn "✗ Backup restore failed from dataset: ${restore_dataset} (exit code: $restore_exit_code)" log_info "Continuing startup without restored data..." fi fi } run_as_node() { local cmd=("$@") if [[ "$(id -u)" -eq 0 ]]; then exec gosu "$OPENCLAW_USER:$OPENCLAW_GROUP" "${cmd[@]}" >>"$OPENCLAW_STDOUT_LOG_PATH" 2>>"$OPENCLAW_STDERR_LOG_PATH" fi exec "${cmd[@]}" >>"$OPENCLAW_STDOUT_LOG_PATH" 2>>"$OPENCLAW_STDERR_LOG_PATH" } run_as_node_background() { log_info "running command in background: $*" local cmd=("$@") log_info "user=$(id -u), OPENCLAW_USER=$OPENCLAW_USER, OPENCLAW_GROUP=$OPENCLAW_GROUP" log_info "command: ${cmd[0]}" if [[ "$(id -u)" -eq 0 ]]; then if command -v gosu >/dev/null 2>&1; then log_info "using gosu: gosu $OPENCLAW_USER:$OPENCLAW_GROUP ${cmd[*]}" gosu "$OPENCLAW_USER:$OPENCLAW_GROUP" "${cmd[@]}" >>"$OPENCLAW_STDOUT_LOG_PATH" 2>>"$OPENCLAW_STDERR_LOG_PATH" & else log_warn "gosu not found, running without gosu" "${cmd[@]}" >>"$OPENCLAW_STDOUT_LOG_PATH" 2>>"$OPENCLAW_STDERR_LOG_PATH" & fi else log_info "not root, running directly: ${cmd[*]}" "${cmd[@]}" >>"$OPENCLAW_STDOUT_LOG_PATH" 2>>"$OPENCLAW_STDERR_LOG_PATH" & fi OPENCLAW_CHILD_PID="$!" log_info "child pid: $OPENCLAW_CHILD_PID" } start_sshx_background() { if ! is_true "$OPENCLAW_SSHX_AUTO_START"; then return 0 fi if ! command -v sshx >/dev/null 2>&1; then echo "openclaw: OPENCLAW_SSHX_AUTO_START=true but sshx not found; skip auto start" >&2 return 0 fi if [[ "$(id -u)" -eq 0 ]]; then gosu "$OPENCLAW_USER:$OPENCLAW_GROUP" sshx >/proc/1/fd/1 2>/proc/1/fd/2 & else sshx >/proc/1/fd/1 2>/proc/1/fd/2 & fi OPENCLAW_SSHX_PID="$!" echo "openclaw: sshx started in background (pid=$OPENCLAW_SSHX_PID)" } stop_sshx_background() { if [[ -n "$OPENCLAW_SSHX_PID" ]] && kill -0 "$OPENCLAW_SSHX_PID" >/dev/null 2>&1; then echo "openclaw: stopping sshx background process (pid=$OPENCLAW_SSHX_PID)" kill "$OPENCLAW_SSHX_PID" >/dev/null 2>&1 || true wait "$OPENCLAW_SSHX_PID" >/dev/null 2>&1 || true fi } OPENCLAW_MANAGER_PID_FILE="/var/run/openclaw/manager.pid" OPENCLAW_SKIP_SHUTDOWN_BACKUP_FILE="/var/run/openclaw/.skip_shutdown_backup" save_pids() { mkdir -p "$(dirname "$OPENCLAW_MANAGER_PID_FILE")" echo "$$" > "$OPENCLAW_MANAGER_PID_FILE" chmod 644 "$OPENCLAW_MANAGER_PID_FILE" } clear_pids() { rm -f "$OPENCLAW_MANAGER_PID_FILE" } backup_on_shutdown() { if [[ -f "$OPENCLAW_SKIP_SHUTDOWN_BACKUP_FILE" ]]; then log_debug "Shutdown backup skipped via flag file" return 0 fi mkdir -p "$(dirname "$OPENCLAW_SKIP_SHUTDOWN_BACKUP_FILE")" touch "$OPENCLAW_SKIP_SHUTDOWN_BACKUP_FILE" log_info "=== Starting Shutdown Backup ===" if [[ -z "${OPENCLAW_BACKUP_DATASET_REPO:-}" ]]; then log_info "Shutdown backup skipped: OPENCLAW_BACKUP_DATASET_REPO is not configured" return 0 fi log_info "Backup dataset: $OPENCLAW_BACKUP_DATASET_REPO" if ! write_backup_env_file; then log_error "Failed to write backup environment file" log_warn "Shutdown backup skipped due to configuration error" return 0 fi log_debug "Backup environment file written: $OPENCLAW_BACKUP_ENV_FILE_PATH" local backup_start_time backup_start_time=$(date +%s) log_info "Executing shutdown backup..." if /usr/local/bin/openclaw-backup-cron.sh; then local backup_end_time backup_end_time=$(date +%s) local backup_duration=$((backup_end_time - backup_start_time)) log_info "✓ Shutdown backup completed successfully (${backup_duration}s)" else local backup_exit_code=$? log_error "✗ Shutdown backup failed (exit code: $backup_exit_code)" log_warn "Container will exit despite backup failure" fi } run_gateway_with_shutdown_backup() { log_info "run_gateway_with_shutdown_backup: $*" log_info "OPENCLAW_GATEWAY_BIND=${OPENCLAW_GATEWAY_BIND}, OPENCLAW_GATEWAY_PORT=${OPENCLAW_GATEWAY_PORT}" log_info "OPENCLAW_STDOUT_LOG_PATH=${OPENCLAW_STDOUT_LOG_PATH}" log_info "OPENCLAW_STDERR_LOG_PATH=${OPENCLAW_STDERR_LOG_PATH}" log_info "PATH=$PATH" log_info "which openclaw: $(command -v openclaw || echo 'not found')" local shutting_down=0 local need_restart=0 local gateway_exit_code=0 wait_for_gateway_ready() { local max_wait=60 local waited=0 log_info "Waiting for gateway to be ready..." while [[ $waited -lt $max_wait ]]; do if curl -sf "http://127.0.0.1:${OPENCLAW_GATEWAY_PORT}/health" >/dev/null 2>&1; then log_info "Gateway is ready after ${waited}s" return 0 fi sleep 1 waited=$((waited + 1)) done log_warn "Gateway ready check timed out after ${max_wait}s, continuing anyway" return 1 } start_gateway() { log_info "starting gateway process (bind=${OPENCLAW_GATEWAY_BIND}, port=${OPENCLAW_GATEWAY_PORT})" run_as_node_background openclaw gateway --allow-unconfigured --bind "$OPENCLAW_GATEWAY_BIND" --port "$OPENCLAW_GATEWAY_PORT" "$@" log_info "gateway process started (pid=${OPENCLAW_CHILD_PID})" save_pids } stop_gateway() { local sig="${1:-TERM}" local gateway_pid gateway_pid=$(pgrep -f "openclaw-gateway$" 2>/dev/null || true) log_info "stopping gateway process (sig=${sig}, pgrep_found=${gateway_pid:-none})" if [[ -n "$gateway_pid" ]]; then kill -s "$sig" "$gateway_pid" 2>/dev/null || true sleep 1 if kill -0 "$gateway_pid" 2>/dev/null; then kill -9 "$gateway_pid" 2>/dev/null || true fi fi } on_manager_signal() { local signal="$1" log_warn "manager received ${signal}" case "$signal" in USR1) log_info "SIGUSR1: will restart gateway after current child exits" need_restart=1 stop_gateway TERM ;; TERM|INT|QUIT) if [[ "$shutting_down" -eq 1 ]]; then return 0 fi shutting_down=1 log_warn "preparing graceful shutdown" stop_gateway TERM stop_sshx_background backup_on_shutdown clear_pids rm -f "$OPENCLAW_MANAGER_PID_FILE" trap - TERM INT QUIT USR1 exit 0 ;; *) ;; esac } trap 'on_manager_signal USR1' USR1 trap 'on_manager_signal TERM' TERM trap 'on_manager_signal INT' INT trap 'on_manager_signal QUIT' QUIT start_gateway "$@" wait_for_gateway_ready while true; do local gateway_pid gateway_pid=$(pgrep -f "openclaw-gateway$" 2>/dev/null || true) if [[ -z "$gateway_pid" ]]; then log_info "gateway process not running, starting..." else log_info "waiting for gateway process (pid=${gateway_pid})..." while kill -0 "$gateway_pid" 2>/dev/null; do sleep 1 done fi log_info "gateway process exited" if [[ "$shutting_down" -eq 1 ]]; then stop_sshx_background backup_on_shutdown clear_pids rm -f "$OPENCLAW_MANAGER_PID_FILE" trap - TERM INT QUIT USR1 exit 0 fi if [[ "$need_restart" -eq 1 ]]; then log_info "restarting gateway (no bootstrap)..." need_restart=0 start_gateway "$@" wait_for_gateway_ready else log_info "gateway exited unexpectedly, waiting for restart signal..." while true; do sleep 1 if [[ "$need_restart" -eq 1 ]]; then log_info "restart signal received" need_restart=0 break fi if [[ "$shutting_down" -eq 1 ]]; then log_info "shutting down" stop_sshx_background backup_on_shutdown clear_pids rm -f "$OPENCLAW_MANAGER_PID_FILE" trap - TERM INT QUIT USR1 exit 0 fi done start_gateway "$@" wait_for_gateway_ready fi done } main() { log_info "══════════════════════════════════════════════════" log_info "OpenClaw Gateway Entrypoint Starting" log_info "══════════════════════════════════════════════════" log_info "Version: ${OPENCLAW_VERSION:-unknown}" log_info "User: $OPENCLAW_USER (UID: $(id -u))" log_info "Home: $OPENCLAW_HOME" log_info "State Dir: $OPENCLAW_STATE_DIR" # Run pre-flight checks first if ! preflight_checks; then log_error "Pre-flight checks failed. Aborting startup." exit 1 fi if [[ "$#" -eq 0 ]]; then set -- gateway fi local subcommand="$1" shift || true if [[ "$subcommand" == "gateway" ]]; then log_info "" log_info "══════════════════════════════════════════════════" log_info "Starting Gateway Bootstrap Sequence" log_info "══════════════════════════════════════════════════" save_pids run_step "prepare runtime directories" mkdir_state_dirs run_step "restore state from backup (if configured)" restore_from_backup_on_startup if [[ "$OPENCLAW_NEED_CONFIG_UPDATE" -eq 1 ]]; then run_step "write or update gateway config" write_or_update_config else log_info "Skipping config write: existing backup found, preserving current configuration" fi run_step "fix file ownership and permissions" fix_permissions run_step "setup SSH agent auto-start (if configured)" setup_ssh_agent_autostart run_step "setup backup cron (if enabled)" setup_backup_cron run_step "start sshx background service (if enabled)" start_sshx_background OPENCLAW_STEP_INDEX=$((OPENCLAW_STEP_INDEX + 1)) log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" log_info "STEP ${OPENCLAW_STEP_INDEX}: launch gateway" log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" run_gateway_with_shutdown_backup "$@" else log_info "Executing subcommand: openclaw ${subcommand}" run_as_node openclaw "$subcommand" "$@" fi } main "$@"