File size: 7,956 Bytes
8756346
 
 
 
fcece3a
8756346
fcece3a
 
8756346
 
 
 
 
 
 
 
 
 
 
 
fcece3a
 
 
 
 
 
 
 
 
 
 
 
66342bb
 
 
0252d6e
66342bb
fcece3a
 
 
 
 
 
 
740ba92
 
fcece3a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8756346
 
 
fcece3a
 
8756346
 
 
 
 
 
fcece3a
8756346
 
fcece3a
 
8756346
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fcece3a
 
8756346
 
 
fcece3a
 
 
 
 
 
 
 
 
 
 
8756346
 
fcece3a
8756346
fcece3a
 
8756346
fcece3a
 
8756346
 
fcece3a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
740ba92
fcece3a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8756346
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
#!/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