Spaces:
Paused
Paused
| # Entrypoint script for Hermes Agent on Hugging Face Spaces | |
| # Based on Hermes Agent real config.yaml format (source: cli-config.yaml.example + hermes_cli/config.py) | |
| # | |
| # Startup architecture: | |
| # entrypoint.sh | |
| # βββ data_sync daemon (background, data persistence) β FIXED: was missing | |
| # βββ Agent Bridge (background, Unix socket IPC) | |
| # βββ hermes gateway run (background, API Server :8642 + messaging platforms) | |
| # βββ node hermes-web-ui (foreground, BFF :7860) | |
| # | |
| set -e | |
| echo "π Hermes Agent v0.10.0 - Hugging Face Spaces" | |
| echo "==============================================" | |
| # Check required environment variables | |
| if [ -z "$HF_DATASET_REPO" ]; then | |
| echo "β οΈ Warning: HF_DATASET_REPO is not set, data will not be persisted to Dataset" | |
| fi | |
| # ==================== INITIALIZE DIRECTORIES ==================== | |
| echo "π Initializing directory structure..." | |
| mkdir -p /data/.hermes/{cron,sessions,logs,memories,skills,pairing,hooks,image_cache,audio_cache,whatsapp/session} | |
| mkdir -p /data/.hermes-web-ui | |
| mkdir -p /app/logs | |
| # ==================== DATA RESTORE ==================== | |
| 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 empty config" | |
| } | |
| fi | |
| # ==================== START DATA SYNC DAEMON ==================== | |
| # FIX: This block was missing from the original entrypoint β auto-backup never ran without it | |
| if [ -n "$HF_DATASET_REPO" ]; then | |
| echo "πΎ Starting data sync daemon..." | |
| python -m src.data_sync daemon & | |
| SYNC_PID=$! | |
| echo " β Sync daemon started (PID: $SYNC_PID, interval: ${SYNC_INTERVAL:-300}s)" | |
| else | |
| echo " β οΈ HF_DATASET_REPO not set, skipping data sync" | |
| SYNC_PID="" | |
| fi | |
| # ==================== SYNC HISTORICAL SESSION MODEL CONFIG ==================== | |
| echo "π Syncing historical session model config..." | |
| python3 << 'SESSION_SYNC' | |
| import json, os, glob | |
| from pathlib import Path | |
| current_model = os.environ.get('HERMES_MODEL', os.environ.get('MODEL_NAME', '')) | |
| if not current_model: | |
| print(" β οΈ No model configured, skipping session sync") | |
| exit(0) | |
| sessions_dir = Path('/data/.hermes/sessions') | |
| if not sessions_dir.exists(): | |
| print(" β οΈ Sessions directory does not exist") | |
| exit(0) | |
| updated = 0 | |
| for session_file in sessions_dir.glob('*.json'): | |
| try: | |
| with open(session_file, 'r', encoding='utf-8') as f: | |
| data = json.load(f) | |
| if 'original_model' not in data and 'model' in data: | |
| data['original_model'] = data['model'] | |
| if data.get('model') != current_model: | |
| data['model'] = current_model | |
| with open(session_file, 'w', encoding='utf-8') as f: | |
| json.dump(data, f, ensure_ascii=False, indent=2) | |
| updated += 1 | |
| except Exception as e: | |
| print(f" β οΈ Failed to process {session_file.name}: {e}") | |
| print(f" β Synced model config for {updated} sessions") | |
| SESSION_SYNC | |
| # ==================== MODEL CONFIGURATION SYSTEM ==================== | |
| echo "π€ Configuring model system..." | |
| # ---- Provider definitions ---- | |
| declare -A PROVIDER_MODELS=( | |
| ["xai"]="grok-4.3" | |
| ["nvidia"]="moonshotai/kimi-k2-thinking" | |
| ["siliconflow"]="deepseek-ai/DeepSeek-V4-Flash" | |
| ["openai"]="gpt-4o" | |
| ["anthropic"]="claude-3-5-sonnet-20241022" | |
| ["google"]="gemini-2.0-flash" | |
| ["gemini"]="gemini-2.5-flash" | |
| ["openrouter"]="meta-llama/llama-3.1-8b-instruct:free" | |
| ["longcat"]="LongCat-Flash-Thinking-2601" | |
| ) | |
| declare -A PROVIDER_API_KEYS=( | |
| ["xai"]="XAI_API_KEY" | |
| ["nvidia"]="NVIDIA_API_KEY" | |
| ["siliconflow"]="SILICONFLOW_API_KEY" | |
| ["openai"]="OPENAI_API_KEY" | |
| ["anthropic"]="ANTHROPIC_API_KEY" | |
| ["google"]="GOOGLE_API_KEY" | |
| ["gemini"]="GEMINI_API_KEY" | |
| ["openrouter"]="OPENROUTER_API_KEY" | |
| ["longcat"]="LONGCAT_API_KEY" | |
| ) | |
| declare -A PROVIDER_BASE_URLS=( | |
| ["xai"]="https://api.x.ai/v1" | |
| ["nvidia"]="https://integrate.api.nvidia.com/v1" | |
| ["siliconflow"]="https://api.siliconflow.cn/v1" | |
| ["openai"]="https://api.openai.com/v1" | |
| ["anthropic"]="https://api.anthropic.com/v1" | |
| ["google"]="https://generativelanguage.googleapis.com" | |
| ["gemini"]="https://generativelanguage.googleapis.com" | |
| ["openrouter"]="https://openrouter.ai/api/v1" | |
| ["longcat"]="https://api.longcat.chat/openai" | |
| ) | |
| # ---- Detect main model ---- | |
| detect_main_model() { | |
| if [ -n "$MODEL_PROVIDER" ] && [ -n "$MODEL_NAME" ]; then | |
| echo "manual:$MODEL_PROVIDER:$MODEL_NAME" | |
| return | |
| fi | |
| for provider in xai nvidia siliconflow openai anthropic google openrouter longcat; do | |
| api_key_var="${PROVIDER_API_KEYS[$provider]}" | |
| if [ -n "${!api_key_var}" ]; then | |
| if [ -n "$MODEL_NAME" ]; then | |
| echo "auto:$provider:$MODEL_NAME" | |
| else | |
| echo "auto:$provider:${PROVIDER_MODELS[$provider]}" | |
| fi | |
| return | |
| fi | |
| done | |
| if [ -n "$GEMINI_API_KEY" ]; then | |
| echo "auto:gemini:${PROVIDER_MODELS[gemini]}" | |
| return | |
| fi | |
| echo "default:nvidia:${PROVIDER_MODELS[nvidia]}" | |
| } | |
| # Map provider name to what Hermes Gateway actually expects | |
| map_provider_for_gateway() { | |
| local provider="$1" | |
| case "$provider" in | |
| siliconflow) | |
| echo "custom" | |
| ;; | |
| *) | |
| echo "$provider" | |
| ;; | |
| esac | |
| } | |
| # ---- Detect auxiliary models ---- | |
| detect_vision_model() { | |
| if [ -n "$VISION_MODEL" ]; then echo "$VISION_MODEL"; return; fi | |
| if [ -n "$NVIDIA_API_KEY" ]; then echo "meta/llama-3.2-11b-vision-instruct"; return; fi | |
| echo "" | |
| } | |
| detect_aux_model() { | |
| if [ -n "$AUX_MODEL" ]; then echo "$AUX_MODEL"; return; fi | |
| if [ -n "$OPENROUTER_API_KEY" ]; then echo "google/gemini-3-flash-preview"; return; fi | |
| if [ -n "$NVIDIA_API_KEY" ]; then echo "openai/gpt-oss-120b"; return; fi | |
| echo "" | |
| } | |
| detect_delegation_model() { | |
| if [ -n "$DELEGATION_MODEL" ]; then echo "$DELEGATION_MODEL"; return; fi | |
| if [ -n "$NVIDIA_API_KEY" ]; then echo "moonshotai/kimi-k2.6"; return; fi | |
| echo "" | |
| } | |
| # ---- Run detection ---- | |
| echo "" | |
| echo "π Model configuration detection:" | |
| echo "ββββββββββββββββββββββββββββββββββββββββ" | |
| MAIN_DETECTED=$(detect_main_model) | |
| IFS=':' read -r MAIN_MODE MAIN_PROVIDER MAIN_MODEL <<< "$MAIN_DETECTED" | |
| echo "π― Main Model: $MAIN_PROVIDER/$MAIN_MODEL (mode: $MAIN_MODE)" | |
| VISION_MODEL_VAL=$(detect_vision_model) | |
| echo "ποΈ Vision Model: ${VISION_MODEL_VAL:-auto-detect}" | |
| AUX_MODEL_VAL=$(detect_aux_model) | |
| echo "β‘ Aux Model: ${AUX_MODEL_VAL:-auto-detect}" | |
| DELEGATION_MODEL_VAL=$(detect_delegation_model) | |
| echo "π» Delegation Model: ${DELEGATION_MODEL_VAL:-inherit-main}" | |
| MAIN_BASE_URL="${PROVIDER_BASE_URLS[$MAIN_PROVIDER]}" | |
| echo " Base URL: $MAIN_BASE_URL" | |
| echo "ββββββββββββββββββββββββββββββββββββββββ" | |
| # ==================== GENERATE config.yaml ==================== | |
| CONFIG_FILE="/data/.hermes/config.yaml" | |
| echo "π Generating config.yaml (Hermes real format)..." | |
| infer_provider() { | |
| local model_id="$1" | |
| if [[ "$model_id" == google/* ]]; then echo "google" | |
| elif [[ "$model_id" == openrouter/* ]]; then echo "openrouter" | |
| elif [[ "$model_id" == Pro/* ]]; then echo "custom" | |
| else echo "$MAIN_PROVIDER"; fi | |
| } | |
| GATEWAY_MAIN_PROVIDER=$(map_provider_for_gateway "$MAIN_PROVIDER") | |
| VISION_PROVIDER_VAL=$(map_provider_for_gateway "$(infer_provider "$VISION_MODEL_VAL")") | |
| AUX_PROVIDER_VAL=$(map_provider_for_gateway "$(infer_provider "$AUX_MODEL_VAL")") | |
| DELEGATION_PROVIDER_VAL=$(map_provider_for_gateway "$(infer_provider "$DELEGATION_MODEL_VAL")") | |
| echo "π Gateway provider mapping: $MAIN_PROVIDER β $GATEWAY_MAIN_PROVIDER" | |
| cat > "$CONFIG_FILE" << EOF | |
| # Hermes Agent Configuration | |
| # Generated by entrypoint.sh at $(date -Iseconds) | |
| # Main model configuration | |
| model: | |
| default: "$MAIN_MODEL" | |
| provider: "$GATEWAY_MAIN_PROVIDER" | |
| base_url: "$MAIN_BASE_URL" | |
| # Auxiliary model configuration (per-task overrides) | |
| auxiliary: | |
| vision: | |
| provider: "${VISION_PROVIDER_VAL:-auto}" | |
| model: "${VISION_MODEL_VAL}" | |
| timeout: 120 | |
| download_timeout: 30 | |
| web_extract: | |
| provider: "${AUX_PROVIDER_VAL:-auto}" | |
| model: "${AUX_MODEL_VAL}" | |
| timeout: 360 | |
| compression: | |
| provider: "${AUX_PROVIDER_VAL:-auto}" | |
| model: "${AUX_MODEL_VAL}" | |
| timeout: 120 | |
| title_generation: | |
| provider: "${AUX_PROVIDER_VAL:-auto}" | |
| model: "${AUX_MODEL_VAL}" | |
| timeout: 30 | |
| session_search: | |
| provider: "auto" | |
| model: "" | |
| timeout: 30 | |
| skills_hub: | |
| provider: "auto" | |
| model: "" | |
| timeout: 30 | |
| approval: | |
| provider: "auto" | |
| model: "" | |
| timeout: 30 | |
| mcp: | |
| provider: "auto" | |
| model: "" | |
| timeout: 30 | |
| flush_memories: | |
| provider: "auto" | |
| model: "" | |
| timeout: 30 | |
| # Sub-agent (Delegation) configuration | |
| delegation: | |
| model: "${DELEGATION_MODEL_VAL}" | |
| provider: "${DELEGATION_PROVIDER_VAL}" | |
| max_iterations: 50 | |
| reasoning_effort: "medium" | |
| # API Server configuration | |
| api_server: | |
| enabled: true | |
| port: 8642 | |
| host: "127.0.0.1" | |
| # Terminal configuration | |
| terminal: | |
| backend: local | |
| timeout: 300 | |
| shell: /bin/bash | |
| # Display configuration | |
| display: | |
| skin: default | |
| show_tool_progress: true | |
| show_resume: true | |
| spinner: dots | |
| # Agent configuration | |
| agent: | |
| max_iterations: 50 | |
| approval_mode: ask | |
| dangerous_command_approval: ask | |
| gateway_timeout: 300 | |
| # Memory configuration | |
| memory: | |
| enabled: true | |
| provider: local | |
| # Compression configuration | |
| compression: | |
| enabled: true | |
| threshold: 0.50 | |
| # Scheduled tasks | |
| cron: | |
| enabled: true | |
| tick_interval: 60 | |
| EOF | |
| echo " β Config file generated" | |
| # ==================== MERGE USER CONFIG ==================== | |
| RESTORED_CONFIG="/data/.hermes/config.yaml.restored" | |
| if [ -f "$RESTORED_CONFIG" ]; then | |
| echo "π Merging user config (platforms, display, agent, etc.)..." | |
| python3 << 'MERGE_SCRIPT' | |
| import yaml | |
| import sys | |
| GENERATED = '/data/.hermes/config.yaml' | |
| RESTORED = '/data/.hermes/config.yaml.restored' | |
| ENTRYPOINT_PRIORITY = {'model', 'auxiliary', 'delegation', 'api_server'} | |
| USER_PRIORITY = {'platforms', 'display', 'agent', 'memory', 'compression', 'cron', 'terminal'} | |
| try: | |
| with open(GENERATED) as f: | |
| generated = yaml.safe_load(f) or {} | |
| with open(RESTORED) as f: | |
| restored = yaml.safe_load(f) or {} | |
| merged = {} | |
| all_keys = set(list(generated.keys()) + list(restored.keys())) | |
| for key in all_keys: | |
| if key in ENTRYPOINT_PRIORITY: | |
| if key in generated: | |
| merged[key] = generated[key] | |
| elif key in USER_PRIORITY: | |
| if key in restored: | |
| merged[key] = restored[key] | |
| elif key in generated: | |
| merged[key] = generated[key] | |
| else: | |
| if key in restored: | |
| merged[key] = restored[key] | |
| elif key in generated: | |
| merged[key] = generated[key] | |
| with open(GENERATED, 'w') as f: | |
| yaml.dump(merged, f, default_flow_style=False, allow_unicode=True, sort_keys=False) | |
| merged_user_keys = [k for k in USER_PRIORITY if k in restored] | |
| print(f" β Merged user sections: {', '.join(merged_user_keys) if merged_user_keys else 'none'}") | |
| except Exception as e: | |
| print(f" β οΈ Config merge failed: {e}, using generated default config") | |
| sys.exit(0) | |
| MERGE_SCRIPT | |
| rm -f "$RESTORED_CONFIG" | |
| else | |
| echo " βΉοΈ No merge needed (no restored user config)" | |
| fi | |
| # ==================== EXPORT PROVIDER ENVIRONMENT VARIABLES ==================== | |
| echo "π Setting provider environment variables..." | |
| for var in XAI_API_KEY NVIDIA_API_KEY SILICONFLOW_API_KEY OPENAI_API_KEY ANTHROPIC_API_KEY GOOGLE_API_KEY GEMINI_API_KEY OPENROUTER_API_KEY LONGCAT_API_KEY; do | |
| if [ -n "${!var}" ]; then | |
| export "$var" | |
| fi | |
| done | |
| if [ -n "$XAI_API_KEY" ]; then | |
| export XAI_BASE_URL="${XAI_BASE_URL:-https://api.x.ai/v1}" | |
| fi | |
| if [ -n "$NVIDIA_API_KEY" ]; then | |
| export NVIDIA_BASE_URL="${NVIDIA_BASE_URL:-https://integrate.api.nvidia.com/v1}" | |
| fi | |
| if [ -n "$SILICONFLOW_API_KEY" ]; then | |
| export SILICONFLOW_BASE_URL="${SILICONFLOW_BASE_URL:-https://api.siliconflow.cn/v1}" | |
| if [ -z "$OPENAI_API_KEY" ]; then | |
| export OPENAI_API_KEY="$SILICONFLOW_API_KEY" | |
| export OPENAI_BASE_URL="${SILICONFLOW_BASE_URL:-https://api.siliconflow.cn/v1}" | |
| echo " βΉοΈ Copied SILICONFLOW_API_KEY to OPENAI_API_KEY (Gateway compatibility)" | |
| fi | |
| fi | |
| if [ -n "$GEMINI_API_KEY" ]; then | |
| export GEMINI_BASE_URL="${GEMINI_BASE_URL:-https://generativelanguage.googleapis.com}" | |
| fi | |
| if [ -n "$OPENROUTER_API_KEY" ]; then | |
| export OPENROUTER_BASE_URL="${OPENROUTER_BASE_URL:-https://openrouter.ai/api/v1}" | |
| fi | |
| if [ -n "$LONGCAT_API_KEY" ]; then | |
| export LONGCAT_BASE_URL="${LONGCAT_BASE_URL:-https://api.longcat.chat/openai}" | |
| fi | |
| # Export API Server environment variables | |
| export API_SERVER_ENABLED=true | |
| export API_SERVER_PORT=8642 | |
| export API_SERVER_HOST=127.0.0.1 | |
| # Generate or reuse API Server key | |
| if [ -n "$API_SERVER_KEY" ]; then | |
| echo " βΉοΈ Using existing API_SERVER_KEY" | |
| else | |
| API_SERVER_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))") | |
| echo " π Generated new API_SERVER_KEY" | |
| fi | |
| export API_SERVER_KEY | |
| # Allow all users by default | |
| export GATEWAY_ALLOW_ALL_USERS="${GATEWAY_ALLOW_ALL_USERS:-true}" | |
| # Export HERMES_HOME | |
| export HERMES_HOME=/data/.hermes | |
| # Export HERMES_MODEL | |
| export HERMES_MODEL="$MAIN_MODEL" | |
| echo " β API Key environment variables exported" | |
| echo " β Base URL environment variables set" | |
| echo " β API Server environment variables set (port: 8642)" | |
| echo " β HERMES_HOME=$HERMES_HOME" | |
| echo " β HERMES_MODEL=$HERMES_MODEL (process-level model override)" | |
| # ==================== INJECT ENVIRONMENT VARIABLES ==================== | |
| echo "βοΈ Injecting environment variables into .env..." | |
| ENV_FILE="/data/.hermes/.env" | |
| mkdir -p /data/.hermes | |
| PERSISTENT_VARS=( | |
| "MODEL_PROVIDER" "MODEL_NAME" "HERMES_MODEL" | |
| "VISION_MODEL" "AUX_MODEL" "DELEGATION_MODEL" | |
| "XAI_API_KEY" "XAI_BASE_URL" | |
| "NVIDIA_API_KEY" "NVIDIA_BASE_URL" | |
| "SILICONFLOW_API_KEY" "SILICONFLOW_BASE_URL" | |
| "OPENAI_API_KEY" | |
| "ANTHROPIC_API_KEY" | |
| "GOOGLE_API_KEY" "GEMINI_API_KEY" "GEMINI_BASE_URL" | |
| "OPENROUTER_API_KEY" "OPENROUTER_BASE_URL" | |
| "LONGCAT_API_KEY" "LONGCAT_BASE_URL" | |
| "API_SERVER_ENABLED" "API_SERVER_PORT" "API_SERVER_HOST" | |
| "TELEGRAM_BOT_TOKEN" "TELEGRAM_ALLOWED_USERS" "TELEGRAM_PROXY" | |
| "DISCORD_BOT_TOKEN" "DISCORD_CLIENT_ID" | |
| "SLACK_BOT_TOKEN" "SLACK_APP_TOKEN" "SLACK_SIGNING_SECRET" | |
| "WHATSAPP_BUSINESS_ID" "WHATSAPP_PHONE_NUMBER" "WHATSAPP_ACCESS_TOKEN" | |
| "WEIXIN_ACCOUNT_ID" "WEIXIN_TOKEN" "WEIXIN_BASE_URL" | |
| "GATEWAY_ALLOW_ALL_USERS" | |
| "AUTH_TOKEN" | |
| "HF_TOKEN" | |
| "HF_DATASET_REPO" | |
| "API_SERVER_ENABLED" "API_SERVER_PORT" "API_SERVER_HOST" "API_SERVER_KEY" | |
| ) | |
| 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 | |
| # Mask ALL API keys before writing to .env (security fix) | |
| { | |
| for key in "${!env_entries[@]}"; do | |
| echo "${env_entries[$key]}" | |
| done | |
| } | sort | python3 -c " | |
| import sys, re | |
| for line in sys.stdin: | |
| line = line.rstrip() | |
| # Mask HF tokens | |
| line = re.sub(r'(HF_TOKEN|HUGGING_FACE_HUB_TOKEN)=hf_[a-zA-Z0-9]+', r'\1=***masked***', line) | |
| line = re.sub(r'(HF_TOKEN|HUGGING_FACE_HUB_TOKEN)=.+', r'\1=***masked***', line) | |
| # Mask all other API keys and secrets | |
| line = re.sub(r'([A-Z_]*(API_KEY|SECRET|TOKEN)[A-Z_]*)=\S+', r'\1=***masked***', line) | |
| print(line) | |
| " > "${ENV_FILE}.masked" | |
| # Write unmasked version to actual .env (for runtime use), masked version for backup | |
| { | |
| for key in "${!env_entries[@]}"; do | |
| echo "${env_entries[$key]}" | |
| done | |
| } | sort > "$ENV_FILE" | |
| RESTORED_COUNT=$(grep -c '=' "$ENV_FILE") | |
| echo " β Written ${RESTORED_COUNT} environment variables (including restored persistent vars)" | |
| # ==================== START AGENT BRIDGE (must be before Gateway) ==================== | |
| echo "π Starting Agent Bridge..." | |
| BRIDGE_ENDPOINT="ipc:///tmp/hermes-agent-bridge.sock" | |
| # Auto-detect correct agent-root (prefer site-packages) | |
| BRIDGE_AGENT_ROOT="" | |
| for candidate in "/usr/local/lib/python3.11/site-packages/" "$HOME/.hermes/hermes-agent" "/opt/hermes/hermes-agent"; do | |
| if [ -f "$candidate/run_agent.py" ] || [ -d "$candidate/hermes_cli" ]; then | |
| BRIDGE_AGENT_ROOT="$candidate" | |
| break | |
| fi | |
| done | |
| if [ -z "$BRIDGE_AGENT_ROOT" ]; then | |
| BRIDGE_AGENT_ROOT="/usr/local/lib/python3.11/site-packages/" | |
| fi | |
| BRIDGE_HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}" | |
| # Ensure bridge script exists | |
| BRIDGE_SCRIPT="/opt/hermes-web-ui/dist/server/agent-bridge/hermes_bridge.py" | |
| if [ -f "$BRIDGE_SCRIPT" ]; then | |
| # Clean up old socket file | |
| rm -f /tmp/hermes-agent-bridge.sock | |
| # Start bridge service (background) | |
| python3 "$BRIDGE_SCRIPT" \ | |
| --endpoint "$BRIDGE_ENDPOINT" \ | |
| --agent-root "$BRIDGE_AGENT_ROOT" \ | |
| --hermes-home "$BRIDGE_HERMES_HOME" \ | |
| --worker-profile default \ | |
| > /data/.hermes/logs/bridge.log 2>&1 & | |
| BRIDGE_PID=$! | |
| echo " β Bridge service started (PID: $BRIDGE_PID, Agent-Root: $BRIDGE_AGENT_ROOT)" | |
| # Wait for bridge socket to be created (up to 10 seconds) | |
| echo " β³ Waiting for Bridge socket to be ready..." | |
| BRIDGE_READY=false | |
| for i in $(seq 1 10); do | |
| if [ -S "/tmp/hermes-agent-bridge.sock" ]; then | |
| BRIDGE_READY=true | |
| break | |
| fi | |
| # Check if process is still alive | |
| if ! kill -0 $BRIDGE_PID 2>/dev/null; then | |
| echo " β οΈ Bridge process exited unexpectedly. Check logs: tail -f /data/.hermes/logs/bridge.log" | |
| break | |
| fi | |
| sleep 1 | |
| done | |
| if [ "$BRIDGE_READY" = true ]; then | |
| echo " β Bridge socket ready: /tmp/hermes-agent-bridge.sock" | |
| else | |
| echo " β οΈ Bridge socket not ready, retrying with fallback agent-root..." | |
| # Try fallback paths | |
| for alt_root in "/usr/local/lib/python3.11/site-packages/" "$HOME/.hermes/hermes-agent"; do | |
| if [ "$alt_root" != "$BRIDGE_AGENT_ROOT" ]; then | |
| rm -f /tmp/hermes-agent-bridge.sock | |
| python3 "$BRIDGE_SCRIPT" --endpoint "$BRIDGE_ENDPOINT" --agent-root "$alt_root" --hermes-home "$BRIDGE_HERMES_HOME" --worker-profile default > /data/.hermes/logs/bridge.log 2>&1 & | |
| BRIDGE_PID=$! | |
| echo " π Trying fallback path: $alt_root (PID: $BRIDGE_PID)" | |
| sleep 2 | |
| if [ -S "/tmp/hermes-agent-bridge.sock" ]; then | |
| echo " β Bridge socket ready at fallback path: $alt_root" | |
| break | |
| fi | |
| fi | |
| done | |
| fi | |
| if [ "$BRIDGE_READY" = true ]; then | |
| # Final check: verify process is still alive | |
| if ! kill -0 $BRIDGE_PID 2>/dev/null; then | |
| echo " β οΈ Bridge process has exited but socket still exists (possible zombie process)" | |
| fi | |
| fi | |
| else | |
| echo " β Bridge script not found: $BRIDGE_SCRIPT" | |
| echo " β οΈ Skipping Bridge startup, but some features may not be available" | |
| fi | |
| # ==================== START GATEWAY (API Server + Messaging Platforms) ==================== | |
| echo "π‘ Starting Hermes Gateway + API Server..." | |
| MAIN_API_KEY_VAR="${PROVIDER_API_KEYS[$MAIN_PROVIDER]}" | |
| MAIN_API_KEY_VAL="${!MAIN_API_KEY_VAR}" | |
| echo "π Gateway configuration diagnostics:" | |
| echo " Original Provider: $MAIN_PROVIDER" | |
| echo " Gateway Provider: $GATEWAY_MAIN_PROVIDER" | |
| echo " Model: $MAIN_MODEL" | |
| echo " Base URL: $MAIN_BASE_URL" | |
| if [ -n "$MAIN_API_KEY_VAL" ]; then | |
| echo " API Key ($MAIN_API_KEY_VAR): set (length: ${#MAIN_API_KEY_VAL})" | |
| else | |
| echo " β οΈ API Key ($MAIN_API_KEY_VAR): not set! Provider authentication may fail" | |
| fi | |
| echo "π Available provider list:" | |
| hermes model 2>/dev/null | head -30 || echo " β οΈ Unable to get provider list" | |
| echo "π§ Running configuration diagnostics:" | |
| hermes doctor 2>/dev/null | grep -E "(provider|model|config|error|warning)" | head -20 || echo " β οΈ hermes doctor not available" | |
| GATEWAY_PIDFILE="/data/.hermes/gateway.pid" | |
| # Gateway wrapper: auto-restart + crash recovery | |
| ( | |
| while true; do | |
| hermes gateway run --replace 2>&1 | while IFS= read -r line; do | |
| echo "$line" | |
| case "$line" in | |
| *"Gateway failed to connect"*) | |
| echo " β οΈ Gateway messaging platform connection failed, API Server still usable, retrying in 30s..." | |
| ;; | |
| esac | |
| done | |
| EXIT_CODE=${PIPESTATUS[0]} | |
| if [ "$EXIT_CODE" -ne 0 ]; then | |
| echo " β οΈ Gateway process exited (code=$EXIT_CODE), restarting in 30s..." | |
| sleep 30 | |
| else | |
| echo " π Gateway exited normally (may have been replaced by BFF restartGateway)" | |
| sleep 5 | |
| if [ -f "$GATEWAY_PIDFILE" ]; then | |
| NEW_PID=$(python3 -c "import json; print(json.load(open('$GATEWAY_PIDFILE')).get('pid',0))" 2>/dev/null || echo 0) | |
| if [ "$NEW_PID" -gt 0 ] && kill -0 "$NEW_PID" 2>/dev/null; then | |
| echo " π Detected new gateway process (PID: $NEW_PID), waiting for it to exit..." | |
| while kill -0 "$NEW_PID" 2>/dev/null; do sleep 5; done | |
| echo " β οΈ New gateway process has exited, restarting wrapper in 30s..." | |
| sleep 30 | |
| continue | |
| fi | |
| fi | |
| echo " π No new gateway process found, not restarting" | |
| break | |
| fi | |
| done | |
| ) & | |
| GATEWAY_PID=$! | |
| # Wait for API Server to be ready | |
| echo " β³ Waiting for API Server to be ready (:8642)..." | |
| API_READY=false | |
| for i in $(seq 1 30); do | |
| if curl -sf http://127.0.0.1:8642/health > /dev/null 2>&1; then | |
| API_READY=true | |
| break | |
| fi | |
| sleep 1 | |
| done | |
| if [ "$API_READY" = true ]; then | |
| echo " β API Server ready (http://127.0.0.1:8642)" | |
| else | |
| echo " β οΈ API Server not ready within 30s, continuing to start Web UI (API Server may become available later)" | |
| fi | |
| if kill -0 $GATEWAY_PID 2>/dev/null; then | |
| echo " β Gateway process running (PID: $GATEWAY_PID)" | |
| else | |
| echo " β οΈ Gateway process has exited, only Web UI will be available" | |
| fi | |
| echo "" | |
| echo "π‘ Tips:" | |
| echo " - Channels page: configure WeChat, Feishu, WeCom and other platforms" | |
| echo " - Models page: manage model providers" | |
| echo " - Jobs page: manage scheduled tasks" | |
| echo "" | |
| # ==================== AUTH TOKEN SETUP ==================== | |
| echo "π Configuring Web UI authentication..." | |
| if [ -z "$AUTH_TOKEN" ]; then | |
| AUTH_TOKEN_FILE="/data/.hermes-web-ui/.token" | |
| if [ -f "$AUTH_TOKEN_FILE" ]; then | |
| AUTH_TOKEN=$(cat "$AUTH_TOKEN_FILE") | |
| echo " β Restored Web UI auth token" | |
| else | |
| AUTH_TOKEN=$(openssl rand -hex 16 2>/dev/null || head -c 32 /dev/urandom | xxd -p | head -c 32) | |
| mkdir -p /data/.hermes-web-ui | |
| echo "$AUTH_TOKEN" > "$AUTH_TOKEN_FILE" | |
| echo "" | |
| echo " ββββββββββββββββββββββββββββββββββββββββββββββββββββ" | |
| echo " β π Web UI Auth Token (save this!) β" | |
| echo " β $AUTH_TOKEN" | |
| echo " β β" | |
| echo " β Enter this token on the Web UI login page β" | |
| echo " β Or set AUTH_TOKEN in HF Spaces Settings β" | |
| echo " ββββββββββββββββββββββββββββββββββββββββββββββββββββ" | |
| echo "" | |
| fi | |
| else | |
| echo " β Using AUTH_TOKEN from environment variable" | |
| fi | |
| export AUTH_TOKEN | |
| # ==================== FIX HERMES-WEB-UI AGENT BRIDGE ==================== | |
| echo "π§ Fixing agent bridge permissions..." | |
| BRIDGE_PATHS=( | |
| "/opt/hermes-web-ui/dist/server/agent-bridge/hermes_bridge.py" | |
| "/usr/lib/node_modules/hermes-web-ui/dist/server/agent-bridge/hermes_bridge.py" | |
| ) | |
| BRIDGE_FOUND=false | |
| for bridge_path in "${BRIDGE_PATHS[@]}"; do | |
| if [ -f "$bridge_path" ]; then | |
| chmod +x "$bridge_path" | |
| echo " β Bridge script permissions fixed: $bridge_path" | |
| BRIDGE_FOUND=true | |
| fi | |
| done | |
| if [ "$BRIDGE_FOUND" = false ]; then | |
| echo " β οΈ Bridge script not found in common paths, trying global search..." | |
| find /opt /usr -name "hermes_bridge.py" -type f 2>/dev/null | while read -r f; do | |
| chmod +x "$f" | |
| echo " β Permissions set: $f" | |
| done | |
| fi | |
| # Ensure hermes module is loadable by bridge | |
| echo "π Locating hermes package path..." | |
| HERMES_PKG_PATH="" | |
| if [ -z "$HERMES_PKG_PATH" ]; then | |
| for candidate in $(find /usr/local/lib/python*/site-packages -maxdepth 1 -type d \( -name "hermes" -o -name "hermes_agent" -o -name "hermes_cli" \) 2>/dev/null); do | |
| if [ -f "$candidate/__init__.py" ]; then | |
| HERMES_PKG_PATH="$candidate" | |
| echo " β Method 1 found hermes package: $HERMES_PKG_PATH" | |
| break | |
| fi | |
| done | |
| fi | |
| if [ -z "$HERMES_PKG_PATH" ]; then | |
| PIP_LOCATION=$(python3 -m pip show hermes-agent 2>/dev/null | grep ^Location | cut -d' ' -f2-) | |
| if [ -n "$PIP_LOCATION" ]; then | |
| for name in hermes hermes_agent hermes_cli; do | |
| if [ -d "$PIP_LOCATION/$name" ] && [ -f "$PIP_LOCATION/$name/__init__.py" ]; then | |
| HERMES_PKG_PATH="$PIP_LOCATION/$name" | |
| echo " β Method 2 found hermes package: $HERMES_PKG_PATH" | |
| break | |
| fi | |
| done | |
| fi | |
| fi | |
| if [ -z "$HERMES_PKG_PATH" ]; then | |
| HERMES_CMD=$(which hermes 2>/dev/null) | |
| if [ -n "$HERMES_CMD" ]; then | |
| HERMES_MODULE=$(head -50 "$HERMES_CMD" 2>/dev/null | grep -E "from|import" | grep -oE "hermes[a-z_]*" | head -1) | |
| if [ -n "$HERMES_MODULE" ]; then | |
| HERMES_TRY=$(python3 -c "import $HERMES_MODULE; print($HERMES_MODULE.__path__[0])" 2>/dev/null) | |
| if [ -n "$HERMES_TRY" ]; then | |
| HERMES_PKG_PATH="$HERMES_TRY" | |
| echo " β Method 3 found hermes package: $HERMES_PKG_PATH (module: $HERMES_MODULE)" | |
| fi | |
| fi | |
| fi | |
| fi | |
| if [ -z "$HERMES_PKG_PATH" ]; then | |
| HERMES_PKG_PATH=$(python3 -c " | |
| import sys, os | |
| for p in sys.path: | |
| for name in ['hermes', 'hermes_agent', 'hermes_cli']: | |
| candidate = os.path.join(p, name) | |
| if os.path.isdir(candidate) and os.path.exists(os.path.join(candidate, '__init__.py')): | |
| print(candidate) | |
| sys.exit(0) | |
| print('') | |
| " 2>/dev/null) | |
| if [ -n "$HERMES_PKG_PATH" ]; then | |
| echo " β Method 4 found hermes package: $HERMES_PKG_PATH" | |
| fi | |
| fi | |
| if [ -z "$HERMES_PKG_PATH" ] && [ -d "/usr/local/lib/hermes-agent" ]; then | |
| HERMES_PKG_PATH="/usr/local/lib/hermes-agent" | |
| echo " β Method 5 found hermes package: $HERMES_PKG_PATH" | |
| fi | |
| HERMES_REAL_HOME=$(realpath ~/.hermes 2>/dev/null || echo "$HOME/.hermes") | |
| mkdir -p "$HERMES_REAL_HOME" | |
| if [ -n "$HERMES_PKG_PATH" ] && [ -d "$HERMES_PKG_PATH" ]; then | |
| if [ ! -e "$HERMES_REAL_HOME/hermes-agent" ] && [ ! -L "$HERMES_REAL_HOME/hermes-agent" ]; then | |
| ln -s "$HERMES_PKG_PATH" "$HERMES_REAL_HOME/hermes-agent" | |
| echo " β hermes-agent module link created β $HERMES_PKG_PATH" | |
| fi | |
| if [ ! -e "$HERMES_REAL_HOME/hermes" ] && [ ! -L "$HERMES_REAL_HOME/hermes" ]; then | |
| ln -s "$HERMES_PKG_PATH" "$HERMES_REAL_HOME/hermes" | |
| echo " β hermes module link created β $HERMES_PKG_PATH" | |
| fi | |
| PYTHON_SITE_PACKAGES=$(python3 -c "import site; print(site.getsitepackages()[0])" 2>/dev/null) | |
| if [ -n "$PYTHON_SITE_PACKAGES" ]; then | |
| export PYTHONPATH="${PYTHONPATH:+$PYTHONPATH:}$PYTHON_SITE_PACKAGES" | |
| echo " β PYTHONPATH set: $PYTHONPATH" | |
| fi | |
| echo "π Testing bridge script..." | |
| if [ -f "$BRIDGE_SCRIPT" ]; then | |
| python3 "$BRIDGE_SCRIPT" --help 2>&1 | head -5 || echo " β οΈ Bridge script test failed" | |
| fi | |
| else | |
| echo " β All methods failed to locate hermes package path!" | |
| echo " π Diagnostic info:" | |
| echo " Python site-packages:" | |
| python3 -c "import site; [print(' ', p) for p in site.getsitepackages()]" 2>/dev/null || true | |
| echo " sys.path:" | |
| python3 -c "import sys; [print(' ', p) for p in sys.path if __import__('os').path.isdir(p)]" 2>/dev/null || true | |
| echo " pip list | grep hermes:" | |
| python3 -m pip list 2>/dev/null | grep -i hermes || echo " (no results)" | |
| echo " which hermes: $(which hermes 2>/dev/null || echo 'not found')" | |
| fi | |
| # ==================== START WEB UI (BFF Server) ==================== | |
| echo "π Starting Hermes Web UI..." | |
| echo " BFF Server: http://0.0.0.0:7860" | |
| echo " Upstream: http://127.0.0.1:8642" | |
| echo "" | |
| export PORT=7860 | |
| export UPSTREAM=http://127.0.0.1:8642 | |
| export HERMES_BIN=/usr/local/bin/hermes | |
| export HERMES_HOME=/data/.hermes | |
| # Graceful shutdown handler | |
| cleanup() { | |
| echo "" | |
| echo "π Running cleanup..." | |
| if [ -n "$HF_DATASET_REPO" ]; then | |
| echo " πΎ Running final data backup..." | |
| python -m src.data_sync backup --force 2>/dev/null || echo " β οΈ Backup failed" | |
| fi | |
| # Stop processes in order: BFF β Bridge β Gateway β Sync | |
| if [ -n "$BRIDGE_PID" ] && kill -0 $BRIDGE_PID 2>/dev/null; then | |
| echo " π Stopping Agent Bridge..." | |
| kill $BRIDGE_PID 2>/dev/null || true | |
| wait $BRIDGE_PID 2>/dev/null || true | |
| rm -f /tmp/hermes-agent-bridge.sock 2>/dev/null | |
| fi | |
| if [ -n "$BFF_PID" ] && kill -0 $BFF_PID 2>/dev/null; then | |
| echo " π Stopping Web UI..." | |
| kill $BFF_PID 2>/dev/null || true | |
| wait $BFF_PID 2>/dev/null || true | |
| fi | |
| if [ -n "$GATEWAY_PID" ] && kill -0 $GATEWAY_PID 2>/dev/null; then | |
| echo " π Stopping Gateway..." | |
| kill $GATEWAY_PID 2>/dev/null || true | |
| wait $GATEWAY_PID 2>/dev/null || true | |
| fi | |
| if [ -n "$SYNC_PID" ] && kill -0 $SYNC_PID 2>/dev/null; then | |
| echo " π Stopping data sync..." | |
| kill $SYNC_PID 2>/dev/null || true | |
| wait $SYNC_PID 2>/dev/null || true | |
| fi | |
| echo "π Goodbye!" | |
| exit 0 | |
| } | |
| trap cleanup SIGTERM SIGINT | |
| # Start BFF Server (bind directly to port 7860) | |
| PORT=7860 node /opt/hermes-web-ui/dist/server/index.js & | |
| BFF_PID=$! | |
| # Wait for BFF to be ready | |
| echo " β³ Waiting for Web UI to be ready..." | |
| BFF_READY=false | |
| for i in $(seq 1 20); do | |
| if curl -sf http://localhost:7860/health > /dev/null 2>&1; then | |
| BFF_READY=true | |
| break | |
| fi | |
| sleep 1 | |
| done | |
| if [ "$BFF_READY" = true ]; then | |
| echo " β Web UI ready β http://localhost:7860" | |
| else | |
| echo " β οΈ Web UI not ready within 20s, check logs" | |
| fi | |
| # Re-verify model config after BFF startup | |
| if [ -f "$CONFIG_FILE" ]; then | |
| if command -v yq &>/dev/null; then | |
| ACTUAL_MODEL=$(yq '.model.default' "$CONFIG_FILE" 2>/dev/null) | |
| if [ -n "$ACTUAL_MODEL" ] && [ "$ACTUAL_MODEL" != "$MAIN_MODEL" ] && [ "$ACTUAL_MODEL" != "null" ]; then | |
| echo " β οΈ Model was overwritten by BFF startup process!" | |
| echo " π Expected: $MAIN_MODEL, Actual: $ACTUAL_MODEL" | |
| echo " π Re-writing correct model config..." | |
| yq -i ".model.default = \"$MAIN_MODEL\"" "$CONFIG_FILE" | |
| yq -i ".model.provider = \"$GATEWAY_MAIN_PROVIDER\"" "$CONFIG_FILE" | |
| yq -i ".model.base_url = \"$MAIN_BASE_URL\"" "$CONFIG_FILE" | |
| echo " β Model corrected: $GATEWAY_MAIN_PROVIDER/$MAIN_MODEL" | |
| elif [ -z "$ACTUAL_MODEL" ] || [ "$ACTUAL_MODEL" = "null" ]; then | |
| echo " β οΈ Model field is empty! Re-writing..." | |
| yq -i ".model.default = \"$MAIN_MODEL\"" "$CONFIG_FILE" | |
| yq -i ".model.provider = \"$GATEWAY_MAIN_PROVIDER\"" "$CONFIG_FILE" | |
| yq -i ".model.base_url = \"$MAIN_BASE_URL\"" "$CONFIG_FILE" | |
| echo " β Model corrected: $GATEWAY_MAIN_PROVIDER/$MAIN_MODEL" | |
| else | |
| echo " β Model config verified: $GATEWAY_MAIN_PROVIDER/$MAIN_MODEL" | |
| fi | |
| fi | |
| fi | |
| # Wait for BFF main process (foreground block β container lifetime controlled by BFF) | |
| wait $BFF_PID | |