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>

Files changed (2) hide show
  1. brain_minimal.py +51 -28
  2. 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
- # Ensure /app is in path for utils import
30
- if "/app" not in sys.path:
31
- sys.path.insert(0, "/app")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.INFO)
56
- logging.warning(f"[BRAIN_MINIMAL] .env initialization failed (using safe defaults): {e}")
 
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 if fallback mode is active
68
- if _fallback_mode:
69
- logger.warning("[BRAIN_MINIMAL] FALLBACK MODE ACTIVE - .env handling may be incomplete")
 
 
 
 
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, create empty .env
56
- _ENV_FILE.touch()
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
- # Create empty .env as last resort
63
- try:
64
- _ENV_FILE.touch()
65
- logger.info("[ENV_INIT] Created empty .env as fallback")
66
- return True
67
- except Exception:
68
- logger.error("[ENV_INIT] Failed to create .env file")
69
- return False
70
- else:
71
- # .env.example doesn't exist, create empty .env
72
- try:
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 init_env():
82
  """
83
- Initialize environment - call this at application startup.
 
84
 
85
- This ensures .env exists before any code tries to load environment variables.
86
- Safe to call multiple times.
87
  """
88
  if not _ENV_FILE.exists():
89
- logger.info("[ENV_INIT] .env missing, initializing...")
90
- _ensure_env_file()
91
- return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)