#!/usr/bin/env python3 """ Environment initialization utility for HuggingClaw Cain. Ensures .env file exists with fallback to .env.example or hardcoded skeleton. This prevents FileNotFoundError when Python modules load before entrypoint.sh runs. Provides robust dotenv loading with safe defaults for critical variables. """ import os import logging from pathlib import Path logger = logging.getLogger(__name__) # Paths _APP_DIR = Path("/app") _ENV_FILE = _APP_DIR / ".env" _ENV_EXAMPLE = _APP_DIR / ".env.example" # Safe defaults for critical variables - these allow Cain to start with partial config _CRITICAL_DEFAULTS = { # Data persistence defaults "OPENCLAW_DATA_DIR": "/data", "OPENCLAW_HOME": "/data/.openclaw", "OPENCLAW_STATE_DIR": "/data/.openclaw", # Server defaults "PORT": "7860", "WORKER_MODE": "auto", "OPENCLAW_PASSWORD": "huggingclaw", # Agent defaults "AGENT_MODE": "active", "WORKER_START_TIMEOUT": "30", "SLEEP_INTERVAL": "5", # Sync defaults "SYNC_INTERVAL": "60", "AUTO_CREATE_DATASET": "false", # LLM defaults (empty - must be set for AI to work) "OPENAI_API_KEY": "", "OPENROUTER_API_KEY": "", "ANTHROPIC_API_KEY": "", "CLAUDE_API_KEY": "", "OPENCLAW_DEFAULT_MODEL": "", # HuggingFace defaults (empty - must be set for data sync to work) "HF_TOKEN": "", "OPENCLAW_DATASET_REPO": "", "SPACE_ID": "tao-shen/HuggingClaw-Cain", } def _get_env_skeleton() -> str: """ Return a hardcoded .env skeleton with safe defaults. This is the ultimate fallback if .env.example is missing. """ lines = [ "# HuggingClaw Cain - Auto-generated .env skeleton", "# Generated because .env.example was not found", "", ] for key, value in _CRITICAL_DEFAULTS.items(): if value: # Only include non-empty defaults lines.append(f"{key}={value}") else: lines.append(f"{key}=") # Include empty key as placeholder return "\n".join(lines) + "\n" def _ensure_env_file() -> bool: """ Ensure .env file exists, using .env.example as fallback, then hardcoded skeleton as ultimate fallback. Returns: True if .env exists or was created, False otherwise. """ # If .env already exists, we're done if _ENV_FILE.exists(): logger.debug("[ENV_INIT] .env already exists") return True logger.warning("[ENV_INIT] .env missing - creating fallback .env file") # Try to copy from .env.example if _ENV_EXAMPLE.exists(): try: # Copy non-empty lines from .env.example to .env # This matches entrypoint.sh behavior (grep -E '^[A-Z_]+=.+[^[:space:]]') lines = [] with open(_ENV_EXAMPLE, "r") as f: for line in f: line = line.strip() # Only include lines with VAR=value (non-empty values) if line and "=" in line and not line.startswith("#"): var_name = line.split("=", 1)[0].strip() if var_name.isupper() and var_name.isidentifier(): # Only include if value is non-empty value = line.split("=", 1)[1].strip() if value and value not in ('""', "''"): lines.append(f"{var_name}={value}\n") if lines: with open(_ENV_FILE, "w") as f: f.writelines(lines) logger.info(f"[ENV_INIT] Created .env from .env.example ({len(lines)} variables)") return True else: # No valid lines found, use skeleton logger.warning("[ENV_INIT] No valid vars in .env.example - using skeleton fallback") except Exception as e: logger.warning(f"[ENV_INIT] Failed to create .env from .env.example: {e}") # .env.example missing or failed - use hardcoded skeleton try: skeleton = _get_env_skeleton() with open(_ENV_FILE, "w") as f: f.write(skeleton) logger.warning("[ENV_INIT] Created .env from hardcoded skeleton (safe defaults applied)") return True except Exception as e: logger.error(f"[ENV_INIT] Failed to create .env from skeleton: {e}") return False def _load_env_file() -> bool: """ Load environment variables from .env file using pure Python. Falls back to safe defaults if loading fails. Returns: True if loading succeeded or defaults were applied, False on critical failure. """ if not _ENV_FILE.exists(): logger.error("[ENV_INIT] Cannot load .env - file does not exist") return False try: # Parse .env file manually (no python-dotenv dependency) with open(_ENV_FILE, "r") as f: for line in f: line = line.strip() # Skip empty lines and comments if not line or line.startswith("#"): continue # Parse VAR=value lines if "=" in line: key, value = line.split("=", 1) key = key.strip() value = value.strip() # Only set if not already in environment (system vars take precedence) if key not in os.environ: os.environ[key] = value logger.info("[ENV_INIT] Loaded environment variables from .env") return True except Exception as e: logger.error(f"[ENV_INIT] Failed to load .env file: {e}") logger.warning("[ENV_INIT] Applying safe defaults for critical variables") # Apply safe defaults as fallback for key, value in _CRITICAL_DEFAULTS.items(): if key not in os.environ: os.environ[key] = value return True # Still return True since we applied defaults def init_env() -> bool: """ Initialize environment - call this at application startup. This ensures .env exists and is loaded with safe defaults. Safe to call multiple times (idempotent). Returns: True if initialization succeeded, False otherwise. """ try: # Step 1: Ensure .env file exists if not _ENV_FILE.exists(): logger.info("[ENV_INIT] .env missing, initializing...") if not _ensure_env_file(): logger.error("[ENV_INIT] Failed to create .env file") # Step 2: Load environment variables if _ENV_FILE.exists(): if not _load_env_file(): logger.warning("[ENV_INIT] .env load failed, but safe defaults applied") # Step 3: Verify critical variables have values (log warnings if missing) _missing_critical = [] _optional_critical = ["HF_TOKEN", "OPENCLAW_DATASET_REPO", "OPENAI_API_KEY", "OPENROUTER_API_KEY", "ANTHROPIC_API_KEY", "CLAUDE_API_KEY"] for key in _optional_critical: if not os.environ.get(key): _missing_critical.append(key) if _missing_critical: logger.warning(f"[ENV_INIT] Optional variables not set (features may be limited): {', '.join(_missing_critical)}") # Log successful initialization logger.info("[ENV_INIT] Environment initialization complete") return True except Exception as e: logger.error(f"[ENV_INIT] Critical error during initialization: {e}") logger.error("[ENV_INIT] Applying emergency safe defaults") # Emergency fallback - set critical defaults directly for key, value in _CRITICAL_DEFAULTS.items(): if key not in os.environ: os.environ[key] = value return True # Still return True to allow app to start # Auto-run on import (safe, idempotent) _init_done = False if not _init_done: init_env() _init_done = True