Spaces:
Sleeping
Sleeping
Claude Code Claude Opus 4.6 commited on
Commit ·
fcece3a
1
Parent(s): 420f732
Claude Code: Improve defensive .env handling with robust error logging
Browse files- Add hardcoded skeleton fallback in env_init.py with safe defaults
- Implement pure-Python .env parsing (no python-dotenv dependency)
- Add explicit error logging for configuration failures
- Ensure critical vars (OPENCLAW_DATA_DIR, PORT, WORKER_MODE) have safe defaults
- Improve brain_minimal.py with better fallback handling and status logging
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- brain_minimal.py +51 -28
- utils/env_init.py +148 -30
brain_minimal.py
CHANGED
|
@@ -25,37 +25,56 @@ from threading import Thread, Event
|
|
| 25 |
# CRITICAL: Defensive .env initialization before any other imports
|
| 26 |
# This prevents FileNotFoundError if brain_minimal.py runs before entrypoint.sh
|
| 27 |
_fallback_mode = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
try:
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
-
# Try to import and run env_init
|
| 34 |
-
try:
|
| 35 |
-
from utils.env_init import init_env
|
| 36 |
-
init_env()
|
| 37 |
-
except ImportError:
|
| 38 |
-
# If utils.env_init is not available, create .env directly
|
| 39 |
-
env_file = Path("/app/.env")
|
| 40 |
-
if not env_file.exists():
|
| 41 |
-
env_example = Path("/app/.env.example")
|
| 42 |
-
if env_example.exists():
|
| 43 |
-
import shutil
|
| 44 |
-
shutil.copy(env_example, env_file)
|
| 45 |
-
logging.basicConfig(level=logging.INFO)
|
| 46 |
-
logging.warning("[BRAIN_MINIMAL] .env not found - copied from .env.example (fallback mode)")
|
| 47 |
-
_fallback_mode = True
|
| 48 |
-
else:
|
| 49 |
-
env_file.touch()
|
| 50 |
-
logging.basicConfig(level=logging.INFO)
|
| 51 |
-
logging.warning("[BRAIN_MINIMAL] .env.example not found - created empty .env (fallback mode)")
|
| 52 |
-
_fallback_mode = True
|
| 53 |
except Exception as e:
|
| 54 |
# Don't crash on .env init errors - log warning and continue
|
| 55 |
-
logging.basicConfig(level=logging.
|
| 56 |
-
logging.
|
|
|
|
| 57 |
_fallback_mode = True
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
# Configure logging
|
| 60 |
logging.basicConfig(
|
| 61 |
level=logging.INFO,
|
|
@@ -64,9 +83,13 @@ logging.basicConfig(
|
|
| 64 |
)
|
| 65 |
logger = logging.getLogger(__name__)
|
| 66 |
|
| 67 |
-
# Log
|
| 68 |
-
if
|
| 69 |
-
logger.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
# Global shutdown flag
|
| 72 |
_shutdown_event = Event()
|
|
|
|
| 25 |
# CRITICAL: Defensive .env initialization before any other imports
|
| 26 |
# This prevents FileNotFoundError if brain_minimal.py runs before entrypoint.sh
|
| 27 |
_fallback_mode = False
|
| 28 |
+
_init_success = False
|
| 29 |
+
|
| 30 |
+
# Ensure /app is in path for utils import
|
| 31 |
+
if "/app" not in sys.path:
|
| 32 |
+
sys.path.insert(0, "/app")
|
| 33 |
+
|
| 34 |
+
# Try to import and run env_init
|
| 35 |
try:
|
| 36 |
+
from utils.env_init import init_env
|
| 37 |
+
_init_success = init_env()
|
| 38 |
+
if not _init_success:
|
| 39 |
+
logging.basicConfig(level=logging.WARNING)
|
| 40 |
+
logging.warning("[BRAIN_MINIMAL] env_init returned False - using emergency defaults")
|
| 41 |
+
_fallback_mode = True
|
| 42 |
+
except ImportError as e:
|
| 43 |
+
# If utils.env_init is not available, use emergency fallback
|
| 44 |
+
logging.basicConfig(level=logging.WARNING)
|
| 45 |
+
logging.error(f"[BRAIN_MINIMAL] Failed to import env_init: {e}")
|
| 46 |
+
logging.warning("[BRAIN_MINIMAL] Using emergency .env fallback (manual initialization)")
|
| 47 |
+
_fallback_mode = True
|
| 48 |
+
|
| 49 |
+
# Manual fallback: create .env if missing
|
| 50 |
+
env_file = Path("/app/.env")
|
| 51 |
+
if not env_file.exists():
|
| 52 |
+
env_example = Path("/app/.env.example")
|
| 53 |
+
if env_example.exists():
|
| 54 |
+
import shutil
|
| 55 |
+
shutil.copy(env_example, env_file)
|
| 56 |
+
logging.warning("[BRAIN_MINIMAL] .env not found - copied from .env.example (emergency fallback)")
|
| 57 |
+
else:
|
| 58 |
+
# Create .env with critical defaults
|
| 59 |
+
with open(env_file, "w") as f:
|
| 60 |
+
f.write("# Emergency fallback .env\n")
|
| 61 |
+
f.write("OPENCLAW_DATA_DIR=/data\n")
|
| 62 |
+
f.write("PORT=7860\n")
|
| 63 |
+
f.write("WORKER_MODE=auto\n")
|
| 64 |
+
logging.warning("[BRAIN_MINIMAL] .env.example not found - created .env with safe defaults (emergency fallback)")
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
except Exception as e:
|
| 67 |
# Don't crash on .env init errors - log warning and continue
|
| 68 |
+
logging.basicConfig(level=logging.WARNING)
|
| 69 |
+
logging.error(f"[BRAIN_MINIMAL] .env initialization failed: {type(e).__name__}: {e}")
|
| 70 |
+
logging.warning("[BRAIN_MINIMAL] Using emergency safe defaults")
|
| 71 |
_fallback_mode = True
|
| 72 |
|
| 73 |
+
# Set emergency defaults directly
|
| 74 |
+
os.environ.setdefault("OPENCLAW_DATA_DIR", "/data")
|
| 75 |
+
os.environ.setdefault("PORT", "7860")
|
| 76 |
+
os.environ.setdefault("WORKER_MODE", "auto")
|
| 77 |
+
|
| 78 |
# Configure logging
|
| 79 |
logging.basicConfig(
|
| 80 |
level=logging.INFO,
|
|
|
|
| 83 |
)
|
| 84 |
logger = logging.getLogger(__name__)
|
| 85 |
|
| 86 |
+
# Log initialization status
|
| 87 |
+
if _init_success:
|
| 88 |
+
logger.info("[BRAIN_MINIMAL] Environment initialized successfully via env_init")
|
| 89 |
+
elif _fallback_mode:
|
| 90 |
+
logger.warning("[BRAIN_MINIMAL] FALLBACK MODE ACTIVE - .env handling used emergency defaults")
|
| 91 |
+
else:
|
| 92 |
+
logger.warning("[BRAIN_MINIMAL] Environment status unknown - proceeding with caution")
|
| 93 |
|
| 94 |
# Global shutdown flag
|
| 95 |
_shutdown_event = Event()
|
utils/env_init.py
CHANGED
|
@@ -2,8 +2,10 @@
|
|
| 2 |
"""
|
| 3 |
Environment initialization utility for HuggingClaw Cain.
|
| 4 |
|
| 5 |
-
Ensures .env file exists with fallback to .env.example.
|
| 6 |
This prevents FileNotFoundError when Python modules load before entrypoint.sh runs.
|
|
|
|
|
|
|
| 7 |
"""
|
| 8 |
import os
|
| 9 |
import logging
|
|
@@ -16,18 +18,67 @@ _APP_DIR = Path("/app")
|
|
| 16 |
_ENV_FILE = _APP_DIR / ".env"
|
| 17 |
_ENV_EXAMPLE = _APP_DIR / ".env.example"
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
def _ensure_env_file() -> bool:
|
| 21 |
"""
|
| 22 |
-
Ensure .env file exists, using .env.example as fallback
|
|
|
|
| 23 |
|
| 24 |
Returns:
|
| 25 |
True if .env exists or was created, False otherwise.
|
| 26 |
"""
|
| 27 |
# If .env already exists, we're done
|
| 28 |
if _ENV_FILE.exists():
|
|
|
|
| 29 |
return True
|
| 30 |
|
|
|
|
|
|
|
| 31 |
# Try to copy from .env.example
|
| 32 |
if _ENV_EXAMPLE.exists():
|
| 33 |
try:
|
|
@@ -52,43 +103,110 @@ def _ensure_env_file() -> bool:
|
|
| 52 |
logger.info(f"[ENV_INIT] Created .env from .env.example ({len(lines)} variables)")
|
| 53 |
return True
|
| 54 |
else:
|
| 55 |
-
# No valid lines found,
|
| 56 |
-
|
| 57 |
-
logger.info("[ENV_INIT] Created empty .env (no valid vars in .env.example)")
|
| 58 |
-
return True
|
| 59 |
|
| 60 |
except Exception as e:
|
| 61 |
logger.warning(f"[ENV_INIT] Failed to create .env from .env.example: {e}")
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
_ENV_FILE.touch()
|
| 74 |
-
logger.info("[ENV_INIT] Created empty .env (.env.example not found)")
|
| 75 |
-
return True
|
| 76 |
-
except Exception as e:
|
| 77 |
-
logger.error(f"[ENV_INIT] Failed to create .env: {e}")
|
| 78 |
-
return False
|
| 79 |
|
| 80 |
|
| 81 |
-
def
|
| 82 |
"""
|
| 83 |
-
|
|
|
|
| 84 |
|
| 85 |
-
|
| 86 |
-
|
| 87 |
"""
|
| 88 |
if not _ENV_FILE.exists():
|
| 89 |
-
logger.
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
|
| 94 |
# Auto-run on import (safe, idempotent)
|
|
|
|
| 2 |
"""
|
| 3 |
Environment initialization utility for HuggingClaw Cain.
|
| 4 |
|
| 5 |
+
Ensures .env file exists with fallback to .env.example or hardcoded skeleton.
|
| 6 |
This prevents FileNotFoundError when Python modules load before entrypoint.sh runs.
|
| 7 |
+
|
| 8 |
+
Provides robust dotenv loading with safe defaults for critical variables.
|
| 9 |
"""
|
| 10 |
import os
|
| 11 |
import logging
|
|
|
|
| 18 |
_ENV_FILE = _APP_DIR / ".env"
|
| 19 |
_ENV_EXAMPLE = _APP_DIR / ".env.example"
|
| 20 |
|
| 21 |
+
# Safe defaults for critical variables - these allow Cain to start with partial config
|
| 22 |
+
_CRITICAL_DEFAULTS = {
|
| 23 |
+
# Data persistence defaults
|
| 24 |
+
"OPENCLAW_DATA_DIR": "/data",
|
| 25 |
+
"OPENCLAW_HOME": "/data/.openclaw",
|
| 26 |
+
"OPENCLAW_STATE_DIR": "/data/.openclaw",
|
| 27 |
+
|
| 28 |
+
# Server defaults
|
| 29 |
+
"PORT": "7860",
|
| 30 |
+
"WORKER_MODE": "auto",
|
| 31 |
+
"OPENCLAW_PASSWORD": "huggingclaw",
|
| 32 |
+
|
| 33 |
+
# Sync defaults
|
| 34 |
+
"SYNC_INTERVAL": "60",
|
| 35 |
+
"AUTO_CREATE_DATASET": "false",
|
| 36 |
+
|
| 37 |
+
# LLM defaults (empty - must be set for AI to work)
|
| 38 |
+
"OPENAI_API_KEY": "",
|
| 39 |
+
"OPENROUTER_API_KEY": "",
|
| 40 |
+
"OPENCLAW_DEFAULT_MODEL": "",
|
| 41 |
+
|
| 42 |
+
# HuggingFace defaults (empty - must be set for data sync to work)
|
| 43 |
+
"HF_TOKEN": "",
|
| 44 |
+
"OPENCLAW_DATASET_REPO": "",
|
| 45 |
+
"SPACE_ID": "tao-shen/HuggingClaw-Cain",
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _get_env_skeleton() -> str:
|
| 50 |
+
"""
|
| 51 |
+
Return a hardcoded .env skeleton with safe defaults.
|
| 52 |
+
This is the ultimate fallback if .env.example is missing.
|
| 53 |
+
"""
|
| 54 |
+
lines = [
|
| 55 |
+
"# HuggingClaw Cain - Auto-generated .env skeleton",
|
| 56 |
+
"# Generated because .env.example was not found",
|
| 57 |
+
"",
|
| 58 |
+
]
|
| 59 |
+
for key, value in _CRITICAL_DEFAULTS.items():
|
| 60 |
+
if value: # Only include non-empty defaults
|
| 61 |
+
lines.append(f"{key}={value}")
|
| 62 |
+
else:
|
| 63 |
+
lines.append(f"{key}=") # Include empty key as placeholder
|
| 64 |
+
return "\n".join(lines) + "\n"
|
| 65 |
+
|
| 66 |
|
| 67 |
def _ensure_env_file() -> bool:
|
| 68 |
"""
|
| 69 |
+
Ensure .env file exists, using .env.example as fallback,
|
| 70 |
+
then hardcoded skeleton as ultimate fallback.
|
| 71 |
|
| 72 |
Returns:
|
| 73 |
True if .env exists or was created, False otherwise.
|
| 74 |
"""
|
| 75 |
# If .env already exists, we're done
|
| 76 |
if _ENV_FILE.exists():
|
| 77 |
+
logger.debug("[ENV_INIT] .env already exists")
|
| 78 |
return True
|
| 79 |
|
| 80 |
+
logger.warning("[ENV_INIT] .env missing - creating fallback .env file")
|
| 81 |
+
|
| 82 |
# Try to copy from .env.example
|
| 83 |
if _ENV_EXAMPLE.exists():
|
| 84 |
try:
|
|
|
|
| 103 |
logger.info(f"[ENV_INIT] Created .env from .env.example ({len(lines)} variables)")
|
| 104 |
return True
|
| 105 |
else:
|
| 106 |
+
# No valid lines found, use skeleton
|
| 107 |
+
logger.warning("[ENV_INIT] No valid vars in .env.example - using skeleton fallback")
|
|
|
|
|
|
|
| 108 |
|
| 109 |
except Exception as e:
|
| 110 |
logger.warning(f"[ENV_INIT] Failed to create .env from .env.example: {e}")
|
| 111 |
+
|
| 112 |
+
# .env.example missing or failed - use hardcoded skeleton
|
| 113 |
+
try:
|
| 114 |
+
skeleton = _get_env_skeleton()
|
| 115 |
+
with open(_ENV_FILE, "w") as f:
|
| 116 |
+
f.write(skeleton)
|
| 117 |
+
logger.warning("[ENV_INIT] Created .env from hardcoded skeleton (safe defaults applied)")
|
| 118 |
+
return True
|
| 119 |
+
except Exception as e:
|
| 120 |
+
logger.error(f"[ENV_INIT] Failed to create .env from skeleton: {e}")
|
| 121 |
+
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
|
| 124 |
+
def _load_env_file() -> bool:
|
| 125 |
"""
|
| 126 |
+
Load environment variables from .env file using pure Python.
|
| 127 |
+
Falls back to safe defaults if loading fails.
|
| 128 |
|
| 129 |
+
Returns:
|
| 130 |
+
True if loading succeeded or defaults were applied, False on critical failure.
|
| 131 |
"""
|
| 132 |
if not _ENV_FILE.exists():
|
| 133 |
+
logger.error("[ENV_INIT] Cannot load .env - file does not exist")
|
| 134 |
+
return False
|
| 135 |
+
|
| 136 |
+
try:
|
| 137 |
+
# Parse .env file manually (no python-dotenv dependency)
|
| 138 |
+
with open(_ENV_FILE, "r") as f:
|
| 139 |
+
for line in f:
|
| 140 |
+
line = line.strip()
|
| 141 |
+
# Skip empty lines and comments
|
| 142 |
+
if not line or line.startswith("#"):
|
| 143 |
+
continue
|
| 144 |
+
# Parse VAR=value lines
|
| 145 |
+
if "=" in line:
|
| 146 |
+
key, value = line.split("=", 1)
|
| 147 |
+
key = key.strip()
|
| 148 |
+
value = value.strip()
|
| 149 |
+
# Only set if not already in environment (system vars take precedence)
|
| 150 |
+
if key not in os.environ:
|
| 151 |
+
os.environ[key] = value
|
| 152 |
+
|
| 153 |
+
logger.info("[ENV_INIT] Loaded environment variables from .env")
|
| 154 |
+
return True
|
| 155 |
+
|
| 156 |
+
except Exception as e:
|
| 157 |
+
logger.error(f"[ENV_INIT] Failed to load .env file: {e}")
|
| 158 |
+
logger.warning("[ENV_INIT] Applying safe defaults for critical variables")
|
| 159 |
+
# Apply safe defaults as fallback
|
| 160 |
+
for key, value in _CRITICAL_DEFAULTS.items():
|
| 161 |
+
if key not in os.environ:
|
| 162 |
+
os.environ[key] = value
|
| 163 |
+
return True # Still return True since we applied defaults
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def init_env() -> bool:
|
| 167 |
+
"""
|
| 168 |
+
Initialize environment - call this at application startup.
|
| 169 |
+
|
| 170 |
+
This ensures .env exists and is loaded with safe defaults.
|
| 171 |
+
Safe to call multiple times (idempotent).
|
| 172 |
+
|
| 173 |
+
Returns:
|
| 174 |
+
True if initialization succeeded, False otherwise.
|
| 175 |
+
"""
|
| 176 |
+
try:
|
| 177 |
+
# Step 1: Ensure .env file exists
|
| 178 |
+
if not _ENV_FILE.exists():
|
| 179 |
+
logger.info("[ENV_INIT] .env missing, initializing...")
|
| 180 |
+
if not _ensure_env_file():
|
| 181 |
+
logger.error("[ENV_INIT] Failed to create .env file")
|
| 182 |
+
|
| 183 |
+
# Step 2: Load environment variables
|
| 184 |
+
if _ENV_FILE.exists():
|
| 185 |
+
if not _load_env_file():
|
| 186 |
+
logger.warning("[ENV_INIT] .env load failed, but safe defaults applied")
|
| 187 |
+
|
| 188 |
+
# Step 3: Verify critical variables have values (log warnings if missing)
|
| 189 |
+
_missing_critical = []
|
| 190 |
+
_optional_critical = ["HF_TOKEN", "OPENCLAW_DATASET_REPO", "OPENAI_API_KEY", "OPENROUTER_API_KEY"]
|
| 191 |
+
for key in _optional_critical:
|
| 192 |
+
if not os.environ.get(key):
|
| 193 |
+
_missing_critical.append(key)
|
| 194 |
+
|
| 195 |
+
if _missing_critical:
|
| 196 |
+
logger.warning(f"[ENV_INIT] Optional variables not set (features may be limited): {', '.join(_missing_critical)}")
|
| 197 |
+
|
| 198 |
+
# Log successful initialization
|
| 199 |
+
logger.info("[ENV_INIT] Environment initialization complete")
|
| 200 |
+
return True
|
| 201 |
+
|
| 202 |
+
except Exception as e:
|
| 203 |
+
logger.error(f"[ENV_INIT] Critical error during initialization: {e}")
|
| 204 |
+
logger.error("[ENV_INIT] Applying emergency safe defaults")
|
| 205 |
+
# Emergency fallback - set critical defaults directly
|
| 206 |
+
for key, value in _CRITICAL_DEFAULTS.items():
|
| 207 |
+
if key not in os.environ:
|
| 208 |
+
os.environ[key] = value
|
| 209 |
+
return True # Still return True to allow app to start
|
| 210 |
|
| 211 |
|
| 212 |
# Auto-run on import (safe, idempotent)
|