#!/bin/bash # Entrypoint script for Hermes Agent on Hugging Face Spaces # # Startup Architecture: # entrypoint.sh # ├── data_sync daemon (Background, Data persistence) # ├── hermes gateway run (Background, API Server :8642 + Gateway) # └── node /opt/hermes-web-ui/dist/server/index.js (Foreground, BFF :7860) set -e echo "🚀 Hermes Agent v0.10.0 - Hugging Face Spaces" echo "==============================================" # ==================== Initialize Directories ==================== echo "📁 Initializing directory structure..." mkdir -p /data/.hermes/{cron,sessions,logs,memories,skills,pairing,hooks,image_cache,audio_cache,whatsapp/session,script} mkdir -p /data/.hermes-web-ui mkdir -p /app/logs # ==================== Data Restoration ==================== export SKIP_CONFIG_RESTORE=true if [ -n "$HF_DATASET_REPO" ]; then echo "📥 Restoring data from Dataset..." python -m src.data_sync restore || { echo "⚠️ Data restore failed, starting with clean configuration" } fi # ==================== Setup config.yaml with Env Expansion ==================== CONFIG_FILE="/data/.hermes/config.yaml" # Expand ${TELEGRAM_BOT_APIBASE} and other environment variables from template into runtime config python3 -c " import os, re src = '/app/config/config.yaml' dst = '/data/.hermes/config.yaml' content = open(src).read() # Expand \${VAR} and \$VAR patterns from current environment def expand(match): var = match.group(1) or match.group(2) return os.environ.get(var, '') expanded = re.sub(r'\\\$\\{([A-Za-z0-9_]+)\\}|\\\$(\\w+)', expand, content) open(dst, 'w').write(expanded) " 2>/dev/null || cp /app/config/config.yaml "$CONFIG_FILE" # Merge restored user config if present RESTORED_CONFIG="/data/.hermes/config.yaml.restored" if [ -f "$RESTORED_CONFIG" ]; then echo "🔄 Merging user configuration..." python3 << 'MERGE_SCRIPT' import yaml GENERATED = '/data/.hermes/config.yaml' RESTORED = '/data/.hermes/config.yaml.restored' ENTRYPOINT_PRIORITY = {'api_server'} try: with open(GENERATED) as f: generated = yaml.safe_load(f) or {} with open(RESTORED) as f: restored = yaml.safe_load(f) or {} merged = {} for k in set(list(generated.keys()) + list(restored.keys())): merged[k] = generated.get(k, restored.get(k)) if k in ENTRYPOINT_PRIORITY else restored.get(k, generated.get(k)) with open(GENERATED, 'w') as f: yaml.dump(merged, f, default_flow_style=False, allow_unicode=True, sort_keys=False) print(" ✅ Configuration successfully merged") except Exception as e: print(f" ⚠️ Merge failed: {e}") MERGE_SCRIPT rm -f "$RESTORED_CONFIG" fi # ==================== REA MCP Registration ==================== # Idempotent; after config generation so the entry survives, before gateway loads config echo "🔌 Registering REA MCP with Hermes..." rea setup --client hermes --yes || echo " ⚠️ REA setup skipped (non-fatal)" rea setup --client opencode --yes || true # optional # ==================== Base URLs & Runtime Environment ==================== export NVIDIA_BASE_URL="${NVIDIA_BASE_URL:-https://integrate.api.nvidia.com/v1}" export API_SERVER_ENABLED=true export API_SERVER_PORT=8642 export API_SERVER_HOST=127.0.0.1 export GATEWAY_ALLOW_ALL_USERS="${GATEWAY_ALLOW_ALL_USERS:-true}" export HERMES_MODEL="${MODEL_NAME:-deepseek-ai/deepseek-v4-flash}" # API Server Secret Key export API_SERVER_KEY="${API_SERVER_KEY:-$(python3 -c "import secrets; print(secrets.token_hex(32))")}" # ==================== Write Environment Variables (.env) ==================== echo "⚙️ Syncing environment variables to .env..." ENV_FILE="/data/.hermes/.env" PERSISTENT_VARS=( "MODEL_PROVIDER" "MODEL_NAME" "HERMES_MODEL" "NVIDIA_API_KEY" "NVIDIA_BASE_URL" "API_SERVER_KEY" "API_SERVER_ENABLED" "API_SERVER_PORT" "API_SERVER_HOST" "TELEGRAM_BOT_TOKEN" "TELEGRAM_ALLOWED_USERS" "TELEGRAM_BOT_APIBASE" "TELEGRAM_PROXY" "GATEWAY_ALLOW_ALL_USERS" "AUTH_TOKEN" "TZ" ) declare -A env_entries=() if [ -f "$ENV_FILE" ]; then while IFS= read -r line; do [[ "$line" =~ ^[[:space:]]*# ]] && continue [[ -z "${line// }" ]] && continue eq_idx="${line%%=*}" if [ -n "$eq_idx" ] && [ "$eq_idx" != "$line" ]; then env_entries["$eq_idx"]="$line" fi done < "$ENV_FILE" fi for var in "${PERSISTENT_VARS[@]}"; do if [ -n "${!var}" ]; then env_entries["$var"]="${var}=${!var}" fi done { for key in "${!env_entries[@]}"; do echo "${env_entries[$key]}" done } | sort > "$ENV_FILE" echo " ✅ .env synchronized" # ==================== Start Data Sync Daemon ==================== SYNC_INTERVAL=${SYNC_INTERVAL:-60} python -m src.data_sync daemon & SYNC_PID=$! echo "🔄 Data sync daemon started (PID: $SYNC_PID, Interval: ${SYNC_INTERVAL}s)" # ==================== Start Gateway & API Server ==================== echo "📡 Starting Hermes Gateway + API Server..." ( while true; do hermes gateway run --replace 2>&1 EXIT_CODE=$? if [ "$EXIT_CODE" -ne 0 ]; then echo " ⚠️ Gateway exited (code=$EXIT_CODE), restarting in 15s..." sleep 15 else echo " 🛑 Gateway exited normally" break fi done ) & GATEWAY_PID=$! echo " ⏳ Waiting for API Server (:8642)..." for i in $(seq 1 30); do if curl -sf http://127.0.0.1:8642/health > /dev/null 2>&1; then echo " ✅ API Server is ready (http://127.0.0.1:8642)" break fi sleep 1 done # ==================== Web UI Auth ==================== export AUTH_TOKEN="${AUTH_TOKEN:-123456}" export PASSWORD="$AUTH_TOKEN" # ==================== Start Zen Proxy (Go) ==================== if [ -d "/data/.hermes/script/opencode-router" ]; then echo "🚀 Initializing & Starting OpenCode Router (:8787)..." chmod +x /data/.hermes/script/opencode-router/opencode-router /data/.hermes/script/opencode-router/*.sh 2>/dev/null || true if [ -f "/data/.hermes/script/opencode-router/start.sh" ]; then bash /data/.hermes/script/opencode-router/start.sh 8787 fi fi # ==================== Start Persistent Services (script/init.d) ==================== [ -f /data/.hermes/script/init.d.sh ] && bash /data/.hermes/script/init.d.sh # ==================== Start Web UI (BFF Server) ==================== echo "🌐 Starting Hermes Web UI on internal port 7861..." export PORT=7861 export UPSTREAM=http://127.0.0.1:8642 export HERMES_BIN=/usr/local/bin/hermes export HERMES_HOME=/data/.hermes export HERMES_WEB_UI_HOME=/data/.hermes-web-ui cleanup() { echo "🛑 Cleaning up..." if [ -n "$HF_DATASET_REPO" ]; then python -m src.data_sync backup --force 2>/dev/null || true fi kill $CADDY_PID $BFF_PID $GATEWAY_PID $SYNC_PID 2>/dev/null || true pkill -f "dufs -c /data/.hermes/script/dufs/dufs.yaml" 2>/dev/null || true pkill -f "code-server --port 8443" 2>/dev/null || true pkill -f "opencode-router 8787" 2>/dev/null || true exit 0 } trap cleanup SIGTERM SIGINT node /opt/hermes-web-ui/dist/server/index.js & BFF_PID=$! echo " ⏳ Waiting for Web UI (:7861)..." for i in $(seq 1 20); do if curl -sf http://localhost:7861/health > /dev/null 2>&1; then echo " ✅ Web UI is ready → http://localhost:7861" break fi sleep 1 done # ==================== Start Caddy Reverse Proxy (:7860) ==================== CADDYFILE="/data/.hermes/script/Caddyfile" if [ ! -f "$CADDYFILE" ]; then mkdir -p /data/.hermes/script cat << 'EOF' > "$CADDYFILE" :7860 { handle_path /code* { reverse_proxy 127.0.0.1:8443 } handle_path /opencode* { reverse_proxy 127.0.0.1:8787 } handle /v1/* { reverse_proxy 127.0.0.1:8642 } handle { reverse_proxy 127.0.0.1:7861 } } EOF fi echo "🔀 Starting Caddy on public port 7860..." caddy run --config "$CADDYFILE" --adapter caddyfile & CADDY_PID=$! echo " ⏳ Waiting for Caddy (:7860)..." for i in $(seq 1 20); do if curl -sf http://localhost:7860/health > /dev/null 2>&1; then echo " ✅ Caddy is ready → http://localhost:7860" break fi sleep 1 done wait $CADDY_PID