HuggingClaw-Cain / utils /env_init.py
Claude Code
Claude Code: Fix worker env injection - add ANTHROPIC_API_KEY and CLAUDE_API_KEY to env config
740ba92
#!/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