Claude Code commited on
Commit
03d0f79
·
1 Parent(s): 5db4585

Fix: Enforce strict uvicorn entrypoint configuration

Browse files
Files changed (2) hide show
  1. Dockerfile +1 -2
  2. entrypoint.sh +1 -233
Dockerfile CHANGED
@@ -19,5 +19,4 @@ EXPOSE 7860
19
  # Set PYTHONPATH to /app
20
  ENV PYTHONPATH=/app
21
 
22
- # Use entrypoint.sh for proper startup sequence (cleans stale errors, starts services)
23
- ENTRYPOINT ["/bin/bash", "/app/entrypoint.sh"]
 
19
  # Set PYTHONPATH to /app
20
  ENV PYTHONPATH=/app
21
 
22
+ CMD ["./entrypoint.sh"]
 
entrypoint.sh CHANGED
@@ -1,234 +1,2 @@
1
  #!/bin/bash
2
- set -e
3
- # Entrypoint for HuggingClaw - Cain
4
-
5
- # Ensure frontend directory exists (defensive: created at build time, but verify at runtime)
6
- mkdir -p /app/frontend
7
- mkdir -p /data/frontend
8
-
9
- # Ensure /data is writable and exists
10
- mkdir -p /data
11
- chmod 777 /data 2>/dev/null || true
12
-
13
- # Configure git identity for commits from within the container
14
- # This ensures commits (like atomic fixes from Cain) succeed
15
- git config --global user.email "cain@huggingface.co"
16
- git config --global user.name "Cain"
17
-
18
- echo "=========================================="
19
- echo "Cain Starting..."
20
- echo "=========================================="
21
- echo "Timestamp: $(date -u +"%Y-%m-%dT%H:%M:%SZ")"
22
- echo "PORT: ${PORT:-7860}"
23
- echo "Working Directory: $(pwd)"
24
- echo "Python Version: $(python --version)"
25
- echo "Data directory: $(ls -la /data 2>&1 | head -5)"
26
- echo "=========================================="
27
-
28
- # Change to app directory
29
- cd /app
30
-
31
- # CRITICAL: Always regenerate .env from .env.example before starting services
32
- # This prevents split-brain state where uvicorn/worker use different env contexts
33
- # when .env exists but is outdated (e.g., HF secrets changed in Space settings)
34
- # FIX: Skip empty values to avoid overriding HF Space secrets with empty strings
35
- if [ -f "/app/.env.example" ]; then
36
- echo "Regenerating .env from .env.example (skipping empty values to preserve HF secrets)..."
37
- # Only include lines that have non-empty values (VAR=value, not VAR=)
38
- # This prevents empty values from overriding HF-injected secrets
39
- grep -E '^[A-Z_]+=.+[^[:space:]]' /app/.env.example > /app/.env 2>/dev/null || touch /app/.env
40
- else
41
- echo "WARNING: .env.example not found, creating empty .env..."
42
- touch /app/.env
43
- fi
44
-
45
- # Load .env file (for local development - ensures worker and uvicorn share env)
46
- echo "Loading .env file..."
47
- set -a # Automatically export all variables
48
- source /app/.env
49
- set +a
50
-
51
- # Set data directory to dataset mount point
52
- export OPENCLAW_DATA_DIR=/data
53
-
54
- # CRITICAL: Explicitly export key environment variables for worker subprocess
55
- # This ensures the brain_minimal.py subprocess has access to all required variables
56
- export WORKER_MODE="${WORKER_MODE:-auto}"
57
- export AGENT_MODE="${AGENT_MODE:-active}"
58
- export SLEEP_INTERVAL="${SLEEP_INTERVAL:-5}"
59
- export WORKER_START_TIMEOUT="${WORKER_START_TIMEOUT:-30}"
60
- export OPENCLAW_DATA_DIR="/data"
61
-
62
- # CRITICAL: Export API keys for error_handlers.py used by worker
63
- # These are required by Claude/Anthropic SDK in the worker process
64
- # Preserves existing values (from HF secrets) or sets empty if not set
65
- export ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY:-}"
66
- export CLAUDE_API_KEY="${CLAUDE_API_KEY:-}"
67
-
68
- # Log environment for debugging (exclude sensitive values)
69
- echo "Environment check:"
70
- echo " WORKER_MODE: ${WORKER_MODE:-not set}"
71
- echo " OPENCLAW_DATASET_REPO: ${OPENCLAW_DATASET_REPO:-not set}"
72
- echo " OPENCLAW_LOG_LEVEL: ${OPENCLAW_LOG_LEVEL:-not set}"
73
- echo " TZ: ${TZ:-not set}"
74
- echo ""
75
-
76
- # Verify app.py exists
77
- if [ ! -f "/app/app.py" ]; then
78
- echo "ERROR: app.py not found in /app"
79
- exit 1
80
- fi
81
- echo "✓ app.py found"
82
-
83
- # List key files for debugging
84
- echo ""
85
- echo "Files in /app:"
86
- ls -la /app/*.py 2>/dev/null || echo " (no .py files in /app root)"
87
-
88
- # Create logs directory
89
- mkdir -p /app/logs
90
-
91
- # Ensure cain_status.json directory exists
92
- mkdir -p /data
93
-
94
- # CRITICAL: Clear stale error field from cain_status.json before startup
95
- # This prevents "unknown" errors from persisting across container restarts
96
- # Clean ALL possible status file locations to ensure consistency
97
- # NOTE: HF Spaces may cache error states - this cleaning runs on every container start
98
- echo "Cleaning stale error field from status files..."
99
- python3 -c "
100
- import json
101
- from pathlib import Path
102
-
103
- status_files = [
104
- Path('/data/cain_status.json'), # Primary (OPENCLAW_DATA_DIR)
105
- Path('/app/openclaw/.openclaw/agents/cain_status.json'), # Nested structure
106
- Path('/app/.openclaw/agents/cain_status.json'), # Legacy flat structure
107
- Path('/app/cain_status.json'), # App root
108
- Path('/app/data/cain_status.json'), # App data subdirectory
109
- Path('/app/memory/cain_status.json'), # Memory directory (CRITICAL: was missing!)
110
- Path('/data/memory/cain_status.json'), # Data memory directory
111
- ]
112
-
113
- cleaned = 0
114
- for status_file in status_files:
115
- try:
116
- if status_file.exists():
117
- with open(status_file, 'r') as f:
118
- data = json.load(f)
119
- error = data.get('error')
120
- # Fix: if error is string 'unknown' or contains 'unknown', set to null
121
- if isinstance(error, str) and error.strip().lower() in ('unknown', 'none', 'null', ''):
122
- data['error'] = None
123
- data['_cleaned_at'] = 'entrypoint'
124
- with open(status_file, 'w') as f:
125
- json.dump(data, f, indent=2)
126
- cleaned += 1
127
- print(f' Cleaned: {status_file}')
128
- elif error is None:
129
- print(f' OK: {status_file} (error already null)')
130
- except Exception as e:
131
- print(f' Could not clean {status_file}: {e}')
132
-
133
- print(f'Cleaned {cleaned} status file(s)')
134
- " 2>/dev/null || echo "(Could not clean status files - continuing)"
135
- echo ""
136
-
137
- echo ""
138
- echo "=========================================="
139
- echo "Starting A2A proxy..."
140
- echo "=========================================="
141
-
142
- # Start a2a-proxy on port 7860 in background (EXTERNAL port)
143
- # Routes A2A traffic to FastAPI backend on internal port
144
- if [ -f "/app/scripts/a2a-proxy.cjs" ]; then
145
- echo "✓ Starting a2a-proxy.cjs on port 7860 (external)..."
146
- OPENCLAW_PORT=7862 LISTEN_PORT=7860 node /app/scripts/a2a-proxy.cjs > /app/logs/a2a-proxy.log 2>&1 &
147
- A2A_PID=$!
148
- echo " A2A proxy PID: $A2A_PID"
149
- # Record PID for cleanup on shutdown
150
- echo $A2A_PID > /tmp/a2a-proxy.pid
151
- else
152
- echo " WARNING: a2a-proxy.cjs not found - A2A routing disabled"
153
- fi
154
-
155
- echo ""
156
- echo "=========================================="
157
- echo "Starting uvicorn..."
158
- echo "=========================================="
159
-
160
- # Start uvicorn on INTERNAL port 7862 (a2a-proxy faces external on 7860)
161
- echo "✓ Starting uvicorn on internal port 7862..."
162
- uvicorn app:app --host 0.0.0.0 --port 7862 > /app/logs/uvicorn.log 2>&1 &
163
- UVICORN_PID=$!
164
- echo " Uvicorn PID: $UVICORN_PID"
165
- echo $UVICORN_PID > /tmp/uvicorn.pid
166
-
167
- # Verify processes are running AND uvicorn is accepting connections
168
- echo ""
169
- echo "=========================================="
170
- echo "Verifying processes..."
171
- echo "=========================================="
172
- sleep 3
173
-
174
- # Check if processes exist
175
- ps aux | grep -E "(uvicorn|node.*a2a-proxy)" | grep -v grep || echo " WARNING: Some processes may not have started"
176
-
177
- # CRITICAL: Verify uvicorn is actually accepting connections on internal port 7862
178
- echo ""
179
- echo "Verifying uvicorn is accepting connections on internal port 7862..."
180
- for i in {1..10}; do
181
- if curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:7862/health | grep -q "200\|401"; then
182
- echo " ✓ Uvicorn is accepting connections on port 7862"
183
- break
184
- else
185
- if [ $i -eq 10 ]; then
186
- echo " ✗ ERROR: Uvicorn not responding after 10 attempts"
187
- echo " Checking uvicorn log:"
188
- tail -20 /app/logs/uvicorn.log 2>&1 || echo " (no uvicorn log found)"
189
- else
190
- echo " Attempt $i/10: uvicorn not ready yet, waiting..."
191
- sleep 2
192
- fi
193
- fi
194
- done
195
-
196
- echo ""
197
- echo "=========================================="
198
- echo "Cain is running!"
199
- echo "=========================================="
200
- echo "A2A Proxy (external): http://0.0.0.0:7860"
201
- echo "FastAPI (internal): http://0.0.0.0:7862"
202
- echo "Worker: Integrated (FastAPI background thread)"
203
- echo ""
204
- echo "PIDs:"
205
- echo " - Uvicorn: $UVICORN_PID"
206
- echo " - A2A Proxy: $A2A_PID"
207
- echo ""
208
-
209
- # Keep container running - monitor background processes and restart if needed
210
- # This prevents the container from exiting when uvicorn crashes
211
- while true; do
212
- # Check if uvicorn is still running
213
- if ! kill -0 $UVICORN_PID 2>/dev/null; then
214
- echo "WARNING: Uvicorn died (PID $UVICORN_PID), restarting..."
215
- uvicorn app:app --host 0.0.0.0 --port 7862 > /app/logs/uvicorn.log 2>&1 &
216
- UVICORN_PID=$!
217
- echo " Uvicorn restarted with PID: $UVICORN_PID"
218
- echo $UVICORN_PID > /tmp/uvicorn.pid
219
- fi
220
-
221
- # Check if a2a-proxy is still running (if it was started)
222
- if [ -n "$A2A_PID" ] && ! kill -0 $A2A_PID 2>/dev/null; then
223
- echo "WARNING: A2A proxy died (PID $A2A_PID), restarting..."
224
- if [ -f "/app/scripts/a2a-proxy.cjs" ]; then
225
- node /app/scripts/a2a-proxy.cjs > /app/logs/a2a-proxy.log 2>&1 &
226
- A2A_PID=$!
227
- echo " A2A proxy restarted with PID: $A2A_PID"
228
- echo $A2A_PID > /tmp/a2a-proxy.pid
229
- fi
230
- fi
231
-
232
- # Sleep before next check
233
- sleep 5
234
- done
 
1
  #!/bin/bash
2
+ exec uvicorn app:app --host 0.0.0.0 --port 7860