Spaces:
Paused
Paused
Download start.sh from DLZZ021/HuggingMes: direct link, hf CLI and curl.
- Browser
- Download file 39.4 kB
-
https://huggingface.co/spaces/DLZZ021/HuggingMes/resolve/main/start.sh
- Command line
-
hf download hf://spaces/DLZZ021/HuggingMes/start.sh
-
curl -L -o start.sh https://huggingface.co/spaces/DLZZ021/HuggingMes/resolve/main/start.sh
39.4 kB
| set -euo pipefail | |
| umask 0077 | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # HuggingMes β Hermes Gateway for HF Spaces | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # ββ Startup Banner ββ | |
| APP_DIR="${HUGGINGMES_APP_DIR:-/opt/huggingmes}" | |
| HERMES_HOME="${HERMES_HOME:-/opt/data}" | |
| PUBLIC_PORT="${PORT:-7861}" | |
| GATEWAY_API_PORT="${API_SERVER_PORT:-8642}" | |
| DASHBOARD_PORT="${DASHBOARD_PORT:-9119}" | |
| TELEGRAM_WEBHOOK_PORT="${TELEGRAM_WEBHOOK_PORT:-8765}" | |
| SYNC_INTERVAL="${SYNC_INTERVAL:-600}" | |
| BACKUP_DATASET="${BACKUP_DATASET_NAME:-huggingmes-backup}" | |
| CF_PROXY_ENV_FILE="/tmp/huggingmes-cloudflare-proxy.env" | |
| STARTUP_FILE="$HERMES_HOME/workspace/startup.sh" | |
| export HERMES_HOME | |
| export API_SERVER_ENABLED="${API_SERVER_ENABLED:-true}" | |
| export API_SERVER_HOST="${API_SERVER_HOST:-127.0.0.1}" | |
| export API_SERVER_PORT="$GATEWAY_API_PORT" | |
| export GATEWAY_HEALTH_URL="${GATEWAY_HEALTH_URL:-http://127.0.0.1:${GATEWAY_API_PORT}}" | |
| export TELEGRAM_WEBHOOK_PORT | |
| export HERMES_GATEWAY_NO_SUPERVISE="${HERMES_GATEWAY_NO_SUPERVISE:-1}" | |
| echo "" | |
| echo " ββββββββββββββββββββββββββββββββββββββββββββ" | |
| echo " β πͺ½ HuggingMes Hermes Gateway β" | |
| echo " ββββββββββββββββββββββββββββββββββββββββββββ" | |
| echo "" | |
| if [ -z "${API_SERVER_KEY:-}" ]; then | |
| if [ -n "${GATEWAY_TOKEN:-}" ]; then | |
| export API_SERVER_KEY="$GATEWAY_TOKEN" | |
| else | |
| API_SERVER_KEY="$(python3 - <<'PY' | |
| import secrets | |
| print(secrets.token_urlsafe(32)) | |
| PY | |
| )" | |
| export API_SERVER_KEY | |
| echo "GATEWAY_TOKEN not set - generated an ephemeral API token for this boot." | |
| fi | |
| fi | |
| # ββ Setup directories ββ | |
| mkdir -p "$HERMES_HOME"/{cron,sessions,logs,hooks,memories,skills,skins,plans,workspace,home,plugins} | |
| # Expose hermes CLI in ~/.local/bin so login shells (terminal backend) find it. | |
| # Base image PATH includes /opt/data/.local/bin but hermes lives in the venv. | |
| mkdir -p "$HERMES_HOME/.local/bin" | |
| ln -sfn /opt/hermes/.venv/bin/hermes "$HERMES_HOME/.local/bin/hermes" | |
| # Redirect Hermes plugin dir into volume so plugins survive container restarts | |
| if [ ! -L "${HOME}/.hermes/plugins" ]; then | |
| mkdir -p "${HOME}/.hermes" | |
| rm -rf "${HOME}/.hermes/plugins" | |
| ln -sfn "$HERMES_HOME/plugins" "${HOME}/.hermes/plugins" | |
| fi | |
| # ββ Restore workspace/state from HF Dataset ββ | |
| if [ -n "${HF_TOKEN:-}" ]; then | |
| echo "Restoring Hermes state from HF Dataset..." | |
| python3 "$APP_DIR/hermes-sync.py" restore || true | |
| else | |
| echo "HF_TOKEN not set - dataset persistence is disabled." | |
| fi | |
| CLOUDFLARE_WORKERS_TOKEN="${CLOUDFLARE_WORKERS_TOKEN:-${CLOUDFLARE_API_TOKEN:-}}" | |
| export CLOUDFLARE_WORKERS_TOKEN | |
| CLOUDFLARE_TUNNEL_TOKEN="${CLOUDFLARE_TUNNEL_TOKEN:-${CF_TUNNEL_TOKEN:-}}" | |
| export CLOUDFLARE_TUNNEL_TOKEN | |
| if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ] || [ -n "${CLOUDFLARE_PROXY_URL:-}" ]; then | |
| export CLOUDFLARE_PROXY_DEBUG="${CLOUDFLARE_PROXY_DEBUG:-false}" | |
| echo "Preparing Cloudflare Telegram proxy..." | |
| python3 "$APP_DIR/cloudflare-proxy-setup.py" || true | |
| if [ -f "$CF_PROXY_ENV_FILE" ]; then | |
| . "$CF_PROXY_ENV_FILE" | |
| fi | |
| fi | |
| if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ]; then | |
| echo "Preparing Cloudflare Keepalive worker..." | |
| python3 "$APP_DIR/cloudflare-keepalive-setup.py" || true | |
| fi | |
| if [ -n "${TELEGRAM_USER_IDS:-}" ] && [ -z "${TELEGRAM_ALLOWED_USERS:-}" ]; then | |
| export TELEGRAM_ALLOWED_USERS="$TELEGRAM_USER_IDS" | |
| elif [ -n "${TELEGRAM_USER_ID:-}" ] && [ -z "${TELEGRAM_ALLOWED_USERS:-}" ]; then | |
| export TELEGRAM_ALLOWED_USERS="$TELEGRAM_USER_ID" | |
| fi | |
| if [ -n "${TELEGRAM_BOT_TOKEN:-}" ] && [ -n "${SPACE_HOST:-}" ] && [ -z "${TELEGRAM_WEBHOOK_URL:-}" ]; then | |
| if [ "${TELEGRAM_MODE:-webhook}" != "polling" ]; then | |
| export TELEGRAM_WEBHOOK_URL="https://${SPACE_HOST}/telegram" | |
| fi | |
| fi | |
| if [ -n "${TELEGRAM_WEBHOOK_URL:-}" ] && [ -z "${TELEGRAM_WEBHOOK_SECRET:-}" ]; then | |
| SECRET_FILE="$HERMES_HOME/.huggingmes-telegram-webhook-secret" | |
| if [ -f "$SECRET_FILE" ]; then | |
| export TELEGRAM_WEBHOOK_SECRET | |
| TELEGRAM_WEBHOOK_SECRET="$(cat "$SECRET_FILE")" | |
| else | |
| TELEGRAM_WEBHOOK_SECRET="$(python3 - <<'PY' | |
| import secrets | |
| print(secrets.token_hex(32)) | |
| PY | |
| )" | |
| printf '%s' "$TELEGRAM_WEBHOOK_SECRET" > "$SECRET_FILE" | |
| chmod 600 "$SECRET_FILE" | |
| export TELEGRAM_WEBHOOK_SECRET | |
| fi | |
| fi | |
| MODEL_INPUT="${HERMES_MODEL:-${LLM_MODEL:-}}" | |
| MODEL_FOR_CONFIG="$MODEL_INPUT" | |
| PROVIDER_FOR_CONFIG="${HERMES_INFERENCE_PROVIDER:-auto}" | |
| LLM_API_KEY="${LLM_API_KEY:-}" | |
| if [ -n "$MODEL_INPUT" ]; then | |
| MODEL_PREFIX="${MODEL_INPUT%%/*}" | |
| else | |
| MODEL_PREFIX="" | |
| fi | |
| case "$MODEL_PREFIX" in | |
| openrouter) | |
| [ -n "$LLM_API_KEY" ] && export OPENROUTER_API_KEY="${OPENROUTER_API_KEY:-$LLM_API_KEY}" | |
| [ "$PROVIDER_FOR_CONFIG" = "auto" ] && PROVIDER_FOR_CONFIG="openrouter" | |
| MODEL_FOR_CONFIG="${MODEL_INPUT#openrouter/}" | |
| ;; | |
| huggingface|hf) | |
| [ -n "$LLM_API_KEY" ] && export HF_TOKEN="${HF_TOKEN:-$LLM_API_KEY}" | |
| [ "$PROVIDER_FOR_CONFIG" = "auto" ] && PROVIDER_FOR_CONFIG="huggingface" | |
| MODEL_FOR_CONFIG="${MODEL_INPUT#huggingface/}" | |
| ;; | |
| vercel-ai-gateway|ai-gateway) | |
| [ -n "$LLM_API_KEY" ] && export AI_GATEWAY_API_KEY="${AI_GATEWAY_API_KEY:-$LLM_API_KEY}" | |
| [ "$PROVIDER_FOR_CONFIG" = "auto" ] && PROVIDER_FOR_CONFIG="ai-gateway" | |
| MODEL_FOR_CONFIG="${MODEL_INPUT#*/}" | |
| ;; | |
| anthropic) | |
| [ -n "$LLM_API_KEY" ] && export ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY:-$LLM_API_KEY}" | |
| ;; | |
| openai|openai-codex) | |
| [ -n "$LLM_API_KEY" ] && export OPENAI_API_KEY="${OPENAI_API_KEY:-$LLM_API_KEY}" | |
| ;; | |
| google|gemini) | |
| [ -n "$LLM_API_KEY" ] && export GOOGLE_API_KEY="${GOOGLE_API_KEY:-$LLM_API_KEY}" GEMINI_API_KEY="${GEMINI_API_KEY:-$LLM_API_KEY}" | |
| PROVIDER_FOR_CONFIG="gemini" | |
| MODEL_FOR_CONFIG="${MODEL_INPUT#*/}" # strip "google/" or "gemini/" prefix β Hermes gemini provider needs bare model name | |
| ;; | |
| deepseek) | |
| [ -n "$LLM_API_KEY" ] && export DEEPSEEK_API_KEY="${DEEPSEEK_API_KEY:-$LLM_API_KEY}" | |
| ;; | |
| kimi-coding|moonshot) | |
| [ -n "$LLM_API_KEY" ] && export KIMI_API_KEY="${KIMI_API_KEY:-$LLM_API_KEY}" | |
| ;; | |
| kimi-coding-cn|moonshot-cn|kimi-cn) | |
| [ -n "$LLM_API_KEY" ] && export KIMI_CN_API_KEY="${KIMI_CN_API_KEY:-$LLM_API_KEY}" | |
| ;; | |
| minimax) | |
| [ -n "$LLM_API_KEY" ] && export MINIMAX_API_KEY="${MINIMAX_API_KEY:-$LLM_API_KEY}" | |
| ;; | |
| minimax-cn) | |
| [ -n "$LLM_API_KEY" ] && export MINIMAX_CN_API_KEY="${MINIMAX_CN_API_KEY:-$LLM_API_KEY}" | |
| ;; | |
| xiaomi) | |
| [ -n "$LLM_API_KEY" ] && export XIAOMI_API_KEY="${XIAOMI_API_KEY:-$LLM_API_KEY}" | |
| ;; | |
| zai|z-ai|z.ai|glm) | |
| [ -n "$LLM_API_KEY" ] && export GLM_API_KEY="${GLM_API_KEY:-$LLM_API_KEY}" | |
| ;; | |
| arcee|arcee-ai|arceeai) | |
| [ -n "$LLM_API_KEY" ] && export ARCEEAI_API_KEY="${ARCEEAI_API_KEY:-$LLM_API_KEY}" | |
| ;; | |
| gmi|gmi-cloud|gmicloud) | |
| [ -n "$LLM_API_KEY" ] && export GMI_API_KEY="${GMI_API_KEY:-$LLM_API_KEY}" | |
| ;; | |
| alibaba) | |
| [ -n "$LLM_API_KEY" ] && export DASHSCOPE_API_KEY="${DASHSCOPE_API_KEY:-$LLM_API_KEY}" | |
| ;; | |
| alibaba-coding-plan|alibaba_coding) | |
| [ -n "$LLM_API_KEY" ] && export DASHSCOPE_API_KEY="${DASHSCOPE_API_KEY:-$LLM_API_KEY}" | |
| ;; | |
| tencent-tokenhub|tencent|tokenhub|tencentmaas) | |
| [ -n "$LLM_API_KEY" ] && export TOKENHUB_API_KEY="${TOKENHUB_API_KEY:-$LLM_API_KEY}" | |
| ;; | |
| nvidia) | |
| [ -n "$LLM_API_KEY" ] && export NVIDIA_API_KEY="${NVIDIA_API_KEY:-$LLM_API_KEY}" | |
| ;; | |
| xai|grok) | |
| [ -n "$LLM_API_KEY" ] && export XAI_API_KEY="${XAI_API_KEY:-$LLM_API_KEY}" | |
| ;; | |
| kilocode) | |
| [ -n "$LLM_API_KEY" ] && export KILOCODE_API_KEY="${KILOCODE_API_KEY:-$LLM_API_KEY}" | |
| ;; | |
| opencode-zen) | |
| [ -n "$LLM_API_KEY" ] && export OPENCODE_ZEN_API_KEY="${OPENCODE_ZEN_API_KEY:-$LLM_API_KEY}" | |
| ;; | |
| opencode-go) | |
| [ -n "$LLM_API_KEY" ] && export OPENCODE_GO_API_KEY="${OPENCODE_GO_API_KEY:-$LLM_API_KEY}" | |
| ;; | |
| mimo|mimo-proxy|xiaomi-mimo) | |
| [ -n "$LLM_API_KEY" ] && export MIMO_API_KEY="${MIMO_API_KEY:-$LLM_API_KEY}" | |
| MIMO_PROXY_AS_PROVIDER="${MIMO_PROXY_AS_PROVIDER:-true}" | |
| [ "$PROVIDER_FOR_CONFIG" = "auto" ] && PROVIDER_FOR_CONFIG="${MIMO_PROXY_PROVIDER:-custom}" | |
| MODEL_FOR_CONFIG="${MODEL_INPUT#*/}" | |
| [ -n "$MODEL_FOR_CONFIG" ] && [ "$MODEL_FOR_CONFIG" != "$MODEL_INPUT" ] || MODEL_FOR_CONFIG="mimo-auto" | |
| ;; | |
| esac | |
| MIMO_PROXY_HOST="${MIMO_PROXY_HOST:-127.0.0.1}" | |
| MIMO_PROXY_PORT="${MIMO_PROXY_PORT:-9100}" | |
| MIMO_PROXY_ENABLED_NORMALIZED="$(printf '%s' "${MIMO_PROXY_ENABLED:-auto}" | tr '[:upper:]' '[:lower:]')" | |
| MIMO_PROXY_AS_PROVIDER_NORMALIZED="$(printf '%s' "${MIMO_PROXY_AS_PROVIDER:-false}" | tr '[:upper:]' '[:lower:]')" | |
| MIMO_PROXY_ACTIVE="false" | |
| case "$MIMO_PROXY_ENABLED_NORMALIZED" in | |
| 1|true|yes|on) MIMO_PROXY_ACTIVE="true" ;; | |
| 0|false|no|off) MIMO_PROXY_ACTIVE="false" ;; | |
| *) | |
| if [ "$MIMO_PROXY_AS_PROVIDER_NORMALIZED" = "true" ] || [ "$MIMO_PROXY_AS_PROVIDER_NORMALIZED" = "1" ] || [ -n "${MIMO_PROXY_KEY:-}" ]; then | |
| MIMO_PROXY_ACTIVE="true" | |
| fi | |
| ;; | |
| esac | |
| if [ "$MIMO_PROXY_ACTIVE" = "true" ]; then | |
| if [ -z "${MIMO_PROXY_KEY:-}" ]; then | |
| MIMO_PROXY_KEY_FILE="$HERMES_HOME/.huggingmes-mimo-proxy-key" | |
| if [ -f "$MIMO_PROXY_KEY_FILE" ]; then | |
| MIMO_PROXY_KEY="$(cat "$MIMO_PROXY_KEY_FILE")" | |
| else | |
| MIMO_PROXY_KEY="$(python3 - <<'PY' | |
| import secrets | |
| print("sk-mimo-" + secrets.token_hex(16)) | |
| PY | |
| )" | |
| printf '%s' "$MIMO_PROXY_KEY" > "$MIMO_PROXY_KEY_FILE" | |
| chmod 600 "$MIMO_PROXY_KEY_FILE" | |
| fi | |
| fi | |
| export MIMO_PROXY_HOST MIMO_PROXY_PORT MIMO_PROXY_KEY MIMO_PROXY_ACTIVE | |
| fi | |
| if [ "$MIMO_PROXY_AS_PROVIDER_NORMALIZED" = "true" ] || [ "$MIMO_PROXY_AS_PROVIDER_NORMALIZED" = "1" ]; then | |
| export MIMO_PROXY_ACTIVE="true" | |
| if [ -z "${MIMO_PROXY_KEY:-}" ]; then | |
| MIMO_PROXY_KEY_FILE="$HERMES_HOME/.huggingmes-mimo-proxy-key" | |
| if [ -f "$MIMO_PROXY_KEY_FILE" ]; then | |
| MIMO_PROXY_KEY="$(cat "$MIMO_PROXY_KEY_FILE")" | |
| else | |
| MIMO_PROXY_KEY="$(python3 - <<'PY' | |
| import secrets | |
| print("sk-mimo-" + secrets.token_hex(16)) | |
| PY | |
| )" | |
| printf '%s' "$MIMO_PROXY_KEY" > "$MIMO_PROXY_KEY_FILE" | |
| chmod 600 "$MIMO_PROXY_KEY_FILE" | |
| fi | |
| export MIMO_PROXY_KEY | |
| fi | |
| PROVIDER_FOR_CONFIG="${MIMO_PROXY_PROVIDER:-custom}" | |
| [ -n "${MODEL_FOR_CONFIG:-}" ] || MODEL_FOR_CONFIG="mimo-auto" | |
| export CUSTOM_BASE_URL="${MIMO_PROXY_BASE_URL:-http://127.0.0.1:${MIMO_PROXY_PORT}/v1}" | |
| export CUSTOM_API_KEY="${MIMO_PROXY_KEY}" | |
| fi | |
| if [ -n "${CUSTOM_BASE_URL:-}" ]; then | |
| if [ -n "${CUSTOM_PROVIDER:-}" ]; then | |
| PROVIDER_FOR_CONFIG="$CUSTOM_PROVIDER" | |
| elif [ -z "${PROVIDER_FOR_CONFIG:-}" ] || [ "$PROVIDER_FOR_CONFIG" = "auto" ]; then | |
| PROVIDER_FOR_CONFIG="custom" | |
| fi | |
| [ -n "$LLM_API_KEY" ] && export OPENAI_API_KEY="${OPENAI_API_KEY:-$LLM_API_KEY}" | |
| fi | |
| export MODEL_FOR_CONFIG PROVIDER_FOR_CONFIG | |
| export CUSTOM_BASE_URL="${CUSTOM_BASE_URL:-}" | |
| export CUSTOM_API_KEY="${CUSTOM_API_KEY:-${LLM_API_KEY:-}}" | |
| export CUSTOM_MODEL_CONTEXT_LENGTH="${CUSTOM_MODEL_CONTEXT_LENGTH:-131072}" | |
| export CUSTOM_MODEL_MAX_TOKENS="${CUSTOM_MODEL_MAX_TOKENS:-8192}" | |
| export TELEGRAM_BASE_URL="${TELEGRAM_BASE_URL:-}" | |
| export TELEGRAM_BASE_FILE_URL="${TELEGRAM_BASE_FILE_URL:-}" | |
| if [ -n "${CLOUDFLARE_PROXY_URL:-}" ] && [ -z "$TELEGRAM_BASE_URL" ]; then | |
| CLOUDFLARE_PROXY_URL="${CLOUDFLARE_PROXY_URL%/}" | |
| export TELEGRAM_BASE_URL="${CLOUDFLARE_PROXY_URL}/bot" | |
| export TELEGRAM_BASE_FILE_URL="${CLOUDFLARE_PROXY_URL}/file/bot" | |
| fi | |
| # ββ Pool key promotion ββ | |
| # Mirror first key from comma-separated pool vars into the singular env var. | |
| # Hermes providers read singular vars; this lets users supply pool keys like | |
| # ANTHROPIC_API_KEYS=key1,key2 and have them picked up automatically. | |
| promote_first_pool_key() { | |
| local singular_var="$1" | |
| local pool_var="$2" | |
| local singular_val="${!singular_var:-}" | |
| local pool_val="${!pool_var:-}" | |
| [ -n "$singular_val" ] && return 0 | |
| [ -n "$pool_val" ] || return 0 | |
| local first | |
| first=$(printf '%s' "$pool_val" | tr ',' '\n' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | awk 'NF{print; exit}') | |
| [ -n "$first" ] || return 0 | |
| export "${singular_var}=$first" | |
| } | |
| promote_first_pool_key "OPENROUTER_API_KEY" "OPENROUTER_API_KEYS" | |
| promote_first_pool_key "ANTHROPIC_API_KEY" "ANTHROPIC_API_KEYS" | |
| promote_first_pool_key "OPENAI_API_KEY" "OPENAI_API_KEYS" | |
| promote_first_pool_key "GOOGLE_API_KEY" "GOOGLE_API_KEYS" | |
| promote_first_pool_key "GEMINI_API_KEY" "GEMINI_API_KEYS" | |
| promote_first_pool_key "DEEPSEEK_API_KEY" "DEEPSEEK_API_KEYS" | |
| promote_first_pool_key "KIMI_API_KEY" "KIMI_API_KEYS" | |
| promote_first_pool_key "MINIMAX_API_KEY" "MINIMAX_API_KEYS" | |
| promote_first_pool_key "NVIDIA_API_KEY" "NVIDIA_API_KEYS" | |
| promote_first_pool_key "XAI_API_KEY" "XAI_API_KEYS" | |
| promote_first_pool_key "KILOCODE_API_KEY" "KILOCODE_API_KEYS" | |
| promote_first_pool_key "GLM_API_KEY" "GLM_API_KEYS" | |
| promote_first_pool_key "ARCEEAI_API_KEY" "ARCEEAI_API_KEYS" | |
| promote_first_pool_key "DASHSCOPE_API_KEY" "DASHSCOPE_API_KEYS" | |
| promote_first_pool_key "GMI_API_KEY" "GMI_API_KEYS" | |
| promote_first_pool_key "TOKENHUB_API_KEY" "TOKENHUB_API_KEYS" | |
| # ββ Build config ββ | |
| python3 - <<'PY' | |
| import os | |
| from pathlib import Path | |
| import yaml | |
| home = Path(os.environ["HERMES_HOME"]) | |
| path = home / "config.yaml" | |
| try: | |
| config = yaml.safe_load(path.read_text(encoding="utf-8")) or {} | |
| except FileNotFoundError: | |
| config = {} | |
| model_name = os.environ.get("MODEL_FOR_CONFIG", "").strip() | |
| provider_name = os.environ.get("PROVIDER_FOR_CONFIG", "").strip() | |
| if model_name: | |
| model = config.setdefault("model", {}) | |
| model["default"] = model_name # always from env β deploy-time setting | |
| if provider_name and provider_name != "auto": | |
| model["provider"] = provider_name # explicit provider (openrouter, huggingface, customβ¦) | |
| else: | |
| model.pop("provider", None) # let Hermes infer from model-name prefix | |
| else: | |
| model = config.get("model", {}) | |
| print("No LLM_MODEL/HERMES_MODEL set; leaving Hermes model config unchanged.") | |
| custom_base = os.environ.get("CUSTOM_BASE_URL", "").strip() | |
| if custom_base and model_name: | |
| model["base_url"] = custom_base.rstrip("/") | |
| if os.environ.get("CUSTOM_API_KEY"): | |
| model["api_key"] = os.environ["CUSTOM_API_KEY"] | |
| try: | |
| model["context_length"] = int(os.environ.get("CUSTOM_MODEL_CONTEXT_LENGTH", "131072")) | |
| model["max_tokens"] = int(os.environ.get("CUSTOM_MODEL_MAX_TOKENS", "8192")) | |
| except ValueError: | |
| pass | |
| config.setdefault("terminal", {}).setdefault("cwd", os.environ.get("MESSAGING_CWD", str(home / "workspace"))) | |
| config.setdefault("compression", {}).setdefault("enabled", True) | |
| config.setdefault("display", {}).setdefault("background_process_notifications", os.environ.get("HERMES_BACKGROUND_NOTIFICATIONS", "result")) | |
| config.setdefault("security", {}).setdefault("redact_secrets", True) | |
| platforms = config.setdefault("platforms", {}) | |
| api_enabled_raw = os.environ.get("API_SERVER_ENABLED", "true").strip().lower() | |
| api_enabled = api_enabled_raw not in {"0", "false", "no", "off"} | |
| api_server = platforms.setdefault("api_server", {}) | |
| api_server["enabled"] = api_enabled | |
| api_extra = api_server.setdefault("extra", {}) | |
| api_extra["host"] = os.environ.get("API_SERVER_HOST", "127.0.0.1") | |
| try: | |
| api_extra["port"] = int(os.environ.get("API_SERVER_PORT", "8642")) | |
| except ValueError: | |
| api_extra["port"] = 8642 | |
| if os.environ.get("API_SERVER_CORS_ORIGINS"): | |
| api_extra["cors_origins"] = [ | |
| item.strip() | |
| for item in os.environ["API_SERVER_CORS_ORIGINS"].split(",") | |
| if item.strip() | |
| ] | |
| if os.environ.get("API_SERVER_MODEL_NAME"): | |
| api_extra["model_name"] = os.environ["API_SERVER_MODEL_NAME"] | |
| if os.environ.get("TELEGRAM_BOT_TOKEN"): | |
| telegram = platforms.setdefault("telegram", {}) | |
| telegram["enabled"] = True | |
| extra = telegram.setdefault("extra", {}) | |
| if os.environ.get("TELEGRAM_BASE_URL"): | |
| extra["base_url"] = os.environ["TELEGRAM_BASE_URL"] | |
| extra["base_file_url"] = os.environ.get("TELEGRAM_BASE_FILE_URL") or os.environ["TELEGRAM_BASE_URL"] | |
| if os.environ.get("TELEGRAM_ALLOWED_USERS"): | |
| config.setdefault("telegram", {}).setdefault("allow_from", [ | |
| item.strip() | |
| for item in os.environ["TELEGRAM_ALLOWED_USERS"].split(",") | |
| if item.strip() | |
| ]) | |
| path.write_text(yaml.safe_dump(config, sort_keys=False), encoding="utf-8") | |
| path.chmod(0o600) | |
| PY | |
| # ββ Startup Summary ββ | |
| HERMES_RUNTIME_VERSION="$(/opt/hermes/.venv/bin/hermes --version 2>/dev/null | awk '{print $NF; exit}' || true)" | |
| echo "" | |
| if [ -n "${HERMES_RUNTIME_VERSION:-}" ]; then | |
| echo "Version : ${HERMES_RUNTIME_VERSION}" | |
| fi | |
| echo "Model : ${MODEL_FOR_CONFIG:-unset}" | |
| echo "Provider : ${PROVIDER_FOR_CONFIG:-unset}" | |
| if [ -n "${TELEGRAM_BOT_TOKEN:-}" ]; then | |
| if [ -n "${TELEGRAM_WEBHOOK_URL:-}" ]; then | |
| echo "Telegram : webhook" | |
| else | |
| echo "Telegram : polling" | |
| fi | |
| else | |
| echo "Telegram : not configured" | |
| fi | |
| if [ -n "${HF_TOKEN:-}" ]; then | |
| echo "Backup : ${BACKUP_DATASET} (every ${SYNC_INTERVAL:-600}s)" | |
| else | |
| echo "Backup : disabled" | |
| fi | |
| if [ -n "${CLOUDFLARE_PROXY_URL:-}" ]; then | |
| echo "Proxy : ${CLOUDFLARE_PROXY_URL}" | |
| fi | |
| if [ "${MIMO_PROXY_ACTIVE:-false}" = "true" ]; then | |
| MIMO_SUMMARY_SUFFIX="" | |
| if [ "${MIMO_PROXY_AS_PROVIDER_NORMALIZED:-false}" = "true" ] || [ "${MIMO_PROXY_AS_PROVIDER_NORMALIZED:-false}" = "1" ]; then | |
| MIMO_SUMMARY_SUFFIX=" (provider)" | |
| fi | |
| echo "MiMo : http://127.0.0.1:${MIMO_PROXY_PORT:-9100}/v1${MIMO_SUMMARY_SUFFIX}" | |
| else | |
| echo "MiMo : disabled" | |
| fi | |
| if [ -n "${SSH_PUBLIC_KEY:-${SSH_AUTHORIZED_KEYS:-}}" ]; then | |
| echo "SSH : ${SSH_BIND_HOST:-127.0.0.1}:${SSH_PORT:-2222} (root, public key only)" | |
| else | |
| echo "SSH : disabled" | |
| fi | |
| if [ -n "${CLOUDFLARE_TUNNEL_TOKEN:-}" ]; then | |
| if [ -n "${CLOUDFLARE_TUNNEL_HOSTNAME:-}" ]; then | |
| echo "CF Tunnel : ${CLOUDFLARE_TUNNEL_HOSTNAME} -> ${SSH_BIND_HOST:-127.0.0.1}:${SSH_PORT:-2222}" | |
| else | |
| echo "CF Tunnel : enabled (token configured)" | |
| fi | |
| else | |
| echo "CF Tunnel : disabled" | |
| fi | |
| echo "Routes : /app/ (Hermes UI), /terminal/ (JupyterLab)" | |
| echo "Dashboard : http://127.0.0.1:${DASHBOARD_PORT}" | |
| echo "Gateway : http://127.0.0.1:${GATEWAY_API_PORT}" | |
| echo "" | |
| # ββ JupyterLab terminal (on by default when GATEWAY_TOKEN is set) ββ | |
| JUPYTER_PID="" | |
| start_jupyter() { | |
| if [ "${DEV_MODE:-true}" = "false" ]; then | |
| echo "JupyterLab disabled (DEV_MODE=false)." | |
| return 0 | |
| fi | |
| # Guard: skip if already running | |
| if [ -n "${JUPYTER_PID:-}" ] && kill -0 "$JUPYTER_PID" 2>/dev/null; then | |
| return 0 | |
| fi | |
| local token="${JUPYTER_TOKEN:-${API_SERVER_KEY:-}}" | |
| if [ -z "$token" ]; then | |
| echo "WARNING: No GATEWAY_TOKEN or JUPYTER_TOKEN set β JupyterLab skipped (terminal would be unauthenticated)." >&2 | |
| return 0 | |
| fi | |
| export JUPYTER_TOKEN="$token" | |
| local VENV_PYTHON="/opt/hermes/.venv/bin/python" | |
| if ! "$VENV_PYTHON" -c "import jupyterlab" >/dev/null 2>&1; then | |
| echo "WARNING: jupyterlab not installed in venv; skipping terminal." >&2 | |
| return 0 | |
| fi | |
| local root_dir="${JUPYTER_ROOT_DIR:-$HERMES_HOME/workspace}" | |
| mkdir -p "$root_dir" | |
| ln -sfn "$HERMES_HOME" "$root_dir/HuggingMes" 2>/dev/null || true | |
| echo "Starting JupyterLab terminal on port 8888 (root: $root_dir)" | |
| "$VENV_PYTHON" -m jupyterlab \ | |
| --ip 127.0.0.1 \ | |
| --port 8888 \ | |
| --no-browser \ | |
| --IdentityProvider.token="$JUPYTER_TOKEN" \ | |
| --ServerApp.base_url=/terminal/ \ | |
| --ServerApp.terminals_enabled=True \ | |
| --ServerApp.terminado_settings='{"shell_command":["/bin/bash","-i"]}' \ | |
| --ServerApp.allow_origin='*' \ | |
| --ServerApp.allow_remote_access=True \ | |
| --ServerApp.trust_xheaders=True \ | |
| --ServerApp.tornado_settings="{'headers': {'Content-Security-Policy': 'frame-ancestors *'}}" \ | |
| --IdentityProvider.cookie_options="{'SameSite': 'None', 'Secure': True}" \ | |
| --ServerApp.disable_check_xsrf=True \ | |
| --LabApp.news_url=None \ | |
| --LabApp.check_for_updates_class=jupyterlab.NeverCheckForUpdate \ | |
| --ServerApp.log_level=WARN \ | |
| --ServerApp.root_dir="$root_dir" \ | |
| >> "$HERMES_HOME/logs/jupyter.log" 2>&1 & | |
| JUPYTER_PID=$! | |
| export JUPYTER_PID | |
| echo "JupyterLab started (PID: $JUPYTER_PID)" | |
| } | |
| MIMO_PROXY_PID="" | |
| start_mimo_proxy_once() { | |
| if [ "${MIMO_PROXY_ACTIVE:-false}" != "true" ]; then | |
| return 0 | |
| fi | |
| if [ -n "${MIMO_PROXY_PID:-}" ] && kill -0 "$MIMO_PROXY_PID" 2>/dev/null; then | |
| return 0 | |
| fi | |
| if [ ! -f "$APP_DIR/mimo-proxy-server.mjs" ]; then | |
| echo "WARNING: mimo-proxy-server.mjs not found; MiMo proxy skipped." >&2 | |
| return 0 | |
| fi | |
| local proxy_log="$HERMES_HOME/logs/mimo-proxy.log" | |
| : > "$proxy_log" | |
| echo "Starting MiMo proxy on ${MIMO_PROXY_HOST:-127.0.0.1}:${MIMO_PROXY_PORT:-9100}..." | |
| MIMO_PROXY_HOST="${MIMO_PROXY_HOST:-127.0.0.1}" \ | |
| MIMO_PROXY_PORT="${MIMO_PROXY_PORT:-9100}" \ | |
| MIMO_PROXY_KEY="${MIMO_PROXY_KEY:?MIMO_PROXY_KEY is required when MiMo proxy is enabled}" \ | |
| node "$APP_DIR/mimo-proxy-server.mjs" >> "$proxy_log" 2>&1 & | |
| MIMO_PROXY_PID=$! | |
| export MIMO_PROXY_PID | |
| echo "MiMo proxy started (PID: $MIMO_PROXY_PID)" | |
| for _ in 1 2 3 4 5; do | |
| if curl -fsS "http://127.0.0.1:${MIMO_PROXY_PORT:-9100}/health" \ | |
| -H "Authorization: Bearer ${MIMO_PROXY_KEY}" >/dev/null 2>&1; then | |
| echo "MiMo proxy health check passed." | |
| return 0 | |
| fi | |
| sleep 1 | |
| done | |
| echo "WARNING: MiMo proxy health check did not pass yet; continuing and watchdog will monitor it." >&2 | |
| } | |
| # Optional root SSH service. HF Spaces do not expose arbitrary TCP ports | |
| # directly, so this binds to localhost by default for later tunneling. | |
| SSHD_PID="" | |
| start_ssh_once() { | |
| local mode="${SSH_ENABLED:-auto}" | |
| if [ "$mode" = "false" ] || [ "$mode" = "0" ] || [ "$mode" = "off" ]; then | |
| return 0 | |
| fi | |
| local authorized_keys="${SSH_PUBLIC_KEY:-${SSH_AUTHORIZED_KEYS:-}}" | |
| if [ -z "$authorized_keys" ]; then | |
| return 0 | |
| fi | |
| if [ -n "${SSHD_PID:-}" ] && kill -0 "$SSHD_PID" 2>/dev/null; then | |
| return 0 | |
| fi | |
| if [ ! -x /usr/sbin/sshd ]; then | |
| echo "WARNING: openssh-server is not installed; SSH service skipped." >&2 | |
| return 0 | |
| fi | |
| local root_cmd=() | |
| if [ "$(id -u)" -ne 0 ]; then | |
| if ! command -v sudo >/dev/null 2>&1 || ! sudo -n true >/dev/null 2>&1; then | |
| echo "WARNING: root/sudo unavailable; SSH service skipped." >&2 | |
| return 0 | |
| fi | |
| root_cmd=(sudo -n) | |
| fi | |
| local ssh_port="${SSH_PORT:-2222}" | |
| local ssh_bind="${SSH_BIND_HOST:-127.0.0.1}" | |
| local ssh_log="$HERMES_HOME/logs/sshd.log" | |
| "${root_cmd[@]}" mkdir -p /run/sshd /root/.ssh /etc/ssh/sshd_config.d | |
| printf '%s\n' "$authorized_keys" | tr -d '\r' | "${root_cmd[@]}" tee /root/.ssh/authorized_keys >/dev/null | |
| "${root_cmd[@]}" chmod 700 /root/.ssh | |
| "${root_cmd[@]}" chmod 600 /root/.ssh/authorized_keys | |
| "${root_cmd[@]}" ssh-keygen -A >/dev/null 2>&1 || true | |
| cat <<EOF | "${root_cmd[@]}" tee /etc/ssh/sshd_config.d/huggingmes.conf >/dev/null | |
| Port ${ssh_port} | |
| ListenAddress ${ssh_bind} | |
| PermitRootLogin prohibit-password | |
| PubkeyAuthentication yes | |
| AuthorizedKeysFile .ssh/authorized_keys | |
| PasswordAuthentication no | |
| KbdInteractiveAuthentication no | |
| ChallengeResponseAuthentication no | |
| PermitEmptyPasswords no | |
| AllowUsers root | |
| AllowTcpForwarding yes | |
| X11Forwarding no | |
| PrintMotd no | |
| EOF | |
| if ! "${root_cmd[@]}" /usr/sbin/sshd -t; then | |
| echo "WARNING: sshd configuration test failed; SSH service skipped." >&2 | |
| return 0 | |
| fi | |
| : > "$ssh_log" | |
| echo "Starting root SSH service on ${ssh_bind}:${ssh_port} (public key only)..." | |
| "${root_cmd[@]}" /usr/sbin/sshd -D -e >> "$ssh_log" 2>&1 & | |
| SSHD_PID=$! | |
| export SSHD_PID | |
| echo "SSH service started (PID: $SSHD_PID)" | |
| } | |
| # Optional Cloudflare Tunnel connector. Configure the tunnel's public hostname | |
| # in Cloudflare Zero Trust as SSH -> 127.0.0.1:${SSH_PORT}. | |
| CLOUDFLARED_PID="" | |
| start_cloudflare_tunnel_once() { | |
| local mode | |
| mode="$(printf '%s' "${CLOUDFLARE_TUNNEL_ENABLED:-auto}" | tr '[:upper:]' '[:lower:]')" | |
| case "$mode" in | |
| 0|false|no|off) return 0 ;; | |
| esac | |
| local token="${CLOUDFLARE_TUNNEL_TOKEN:-${CF_TUNNEL_TOKEN:-}}" | |
| if [ -z "$token" ]; then | |
| if [ "$mode" = "1" ] || [ "$mode" = "true" ] || [ "$mode" = "yes" ] || [ "$mode" = "on" ]; then | |
| echo "WARNING: CLOUDFLARE_TUNNEL_ENABLED=true but CLOUDFLARE_TUNNEL_TOKEN is not set." >&2 | |
| fi | |
| return 0 | |
| fi | |
| if [ -n "${CLOUDFLARED_PID:-}" ] && kill -0 "$CLOUDFLARED_PID" 2>/dev/null; then | |
| return 0 | |
| fi | |
| if ! command -v cloudflared >/dev/null 2>&1; then | |
| echo "WARNING: cloudflared is not installed; Cloudflare Tunnel skipped." >&2 | |
| return 0 | |
| fi | |
| if [ -z "${SSH_PUBLIC_KEY:-${SSH_AUTHORIZED_KEYS:-}}" ]; then | |
| echo "WARNING: Cloudflare Tunnel token is set, but SSH_PUBLIC_KEY is empty; SSH upstream may be unavailable." >&2 | |
| fi | |
| local tunnel_log="$HERMES_HOME/logs/cloudflared.log" | |
| local protocol="${CLOUDFLARE_TUNNEL_PROTOCOL:-http2}" | |
| : > "$tunnel_log" | |
| if [ -n "${CLOUDFLARE_TUNNEL_HOSTNAME:-}" ]; then | |
| echo "Starting Cloudflare Tunnel for ${CLOUDFLARE_TUNNEL_HOSTNAME}..." | |
| else | |
| echo "Starting Cloudflare Tunnel..." | |
| fi | |
| cloudflared tunnel --no-autoupdate --protocol "$protocol" run --token "$token" >> "$tunnel_log" 2>&1 & | |
| CLOUDFLARED_PID=$! | |
| export CLOUDFLARED_PID | |
| echo "Cloudflare Tunnel started (PID: $CLOUDFLARED_PID)" | |
| sleep 2 | |
| if ! kill -0 "$CLOUDFLARED_PID" 2>/dev/null; then | |
| echo "WARNING: Cloudflare Tunnel exited during startup. Last 40 log lines:" >&2 | |
| tail -40 "$tunnel_log" >&2 || true | |
| fi | |
| } | |
| # ββ Trap SIGTERM for graceful shutdown ββ | |
| SYNC_LOOP_PID="" | |
| DASHBOARD_PID="" | |
| graceful_shutdown() { | |
| echo "Shutting down HuggingMes..." | |
| if [ -n "${HF_TOKEN:-}" ]; then | |
| python3 "$APP_DIR/hermes-sync.py" sync-once || echo "Warning: shutdown sync failed." | |
| fi | |
| kill $(jobs -p) 2>/dev/null || true | |
| exit 0 | |
| } | |
| trap graceful_shutdown SIGTERM SIGINT | |
| # ββ Shell capture wrappers ββ | |
| # Written to ~/.bashrc so terminal installs are recorded in workspace/startup.sh | |
| # and replayed on next boot β packages survive Space restarts. | |
| if [ ! -f "$STARTUP_FILE" ]; then | |
| touch "$STARTUP_FILE" | |
| chmod +x "$STARTUP_FILE" | |
| echo "Created workspace/startup.sh" | |
| fi | |
| cat > "$HOME/.bashrc" << 'BASHRC' | |
| export PATH="/opt/hermes/.venv/bin:/opt/data/.local/bin:$PATH" | |
| export DEBIAN_FRONTEND="${DEBIAN_FRONTEND:-noninteractive}" | |
| if [ -z "${PS1:-}" ] || [ "$PS1" = "$ " ]; then | |
| export PS1="\u@\h:\w\$ " | |
| fi | |
| HERMES_HOME="${HERMES_HOME:-/opt/data}" | |
| STARTUP_FILE="$HERMES_HOME/workspace/startup.sh" | |
| _hm_append() { | |
| [ "${HUGGINGMES_CAPTURE_DISABLE:-0}" = "1" ] && return 0 | |
| local line="$*" | |
| mkdir -p "$(dirname "$STARTUP_FILE")" | |
| touch "$STARTUP_FILE" | |
| chmod +x "$STARTUP_FILE" 2>/dev/null || true | |
| grep -qxF "$line" "$STARTUP_FILE" 2>/dev/null || echo "$line" >> "$STARTUP_FILE" | |
| } | |
| _hm_quote_args() { | |
| local quoted=() | |
| local arg | |
| for arg in "$@"; do | |
| printf -v arg '%q' "$arg" | |
| quoted+=("$arg") | |
| done | |
| printf '%s' "${quoted[*]}" | |
| } | |
| _hm_append_cmd() { | |
| local cmd="$1" | |
| shift | |
| local args | |
| args=$(_hm_quote_args "$@") | |
| if [ -n "$args" ]; then | |
| _hm_append "$cmd $args" | |
| else | |
| _hm_append "$cmd" | |
| fi | |
| } | |
| _hm_args_without_flags() { | |
| local out=() | |
| for arg in "$@"; do | |
| case "$arg" in | |
| ''|-|--*|-*) ;; | |
| *) out+=("$arg") ;; | |
| esac | |
| done | |
| printf '%s\n' "${out[@]}" | |
| } | |
| _hm_has_install_targets() { | |
| local item | |
| while IFS= read -r item; do | |
| [ -n "$item" ] && return 0 | |
| done <<EOF | |
| $(_hm_args_without_flags "$@") | |
| EOF | |
| return 1 | |
| } | |
| _hm_has_arg() { | |
| local needle="$1" | |
| shift | |
| for arg in "$@"; do | |
| [ "$arg" = "$needle" ] && return 0 | |
| done | |
| return 1 | |
| } | |
| _hm_can_sudo_apt() { | |
| command -v sudo >/dev/null 2>&1 && sudo -n apt-get --version >/dev/null 2>&1 | |
| } | |
| _hm_apt_install() { | |
| if [ "$(id -u)" -eq 0 ]; then | |
| command apt-get update && command apt-get install -y "$@" | |
| elif _hm_can_sudo_apt; then | |
| sudo apt-get update && sudo apt-get install -y "$@" | |
| else | |
| echo "Error: apt install needs root." >&2 | |
| return 1 | |
| fi | |
| } | |
| apt-get() { | |
| case "${1:-}" in | |
| install) | |
| shift | |
| _hm_apt_install "$@" | |
| local rc=$? | |
| if [ $rc -eq 0 ]; then | |
| _hm_has_install_targets "$@" && _hm_append_cmd "sudo apt-get update && sudo apt-get install -y" "$@" | |
| fi | |
| return $rc | |
| ;; | |
| update) | |
| if [ "$(id -u)" -eq 0 ]; then command apt-get "$@" | |
| elif _hm_can_sudo_apt; then sudo apt-get "$@" | |
| else command apt-get "$@"; fi | |
| return $? | |
| ;; | |
| *) command apt-get "$@"; return $? ;; | |
| esac | |
| } | |
| apt() { | |
| case "${1:-}" in | |
| install) | |
| shift | |
| _hm_apt_install "$@" | |
| local rc=$? | |
| if [ $rc -eq 0 ]; then | |
| _hm_has_install_targets "$@" && _hm_append_cmd "sudo apt-get update && sudo apt-get install -y" "$@" | |
| fi | |
| return $rc | |
| ;; | |
| update) | |
| if [ "$(id -u)" -eq 0 ]; then command apt "$@" | |
| elif _hm_can_sudo_apt; then sudo apt "$@" | |
| else command apt "$@"; fi | |
| return $? | |
| ;; | |
| *) command apt "$@"; return $? ;; | |
| esac | |
| } | |
| pip() { | |
| command pip "$@" | |
| local rc=$? | |
| if [ $rc -eq 0 ] && [ "${1:-}" = "install" ] \ | |
| && ! _hm_has_arg -r "${@:2}" && ! _hm_has_arg --requirement "${@:2}" \ | |
| && _hm_has_install_targets "${@:2}"; then | |
| _hm_append_cmd "pip install" "${@:2}" | |
| fi | |
| return $rc | |
| } | |
| pip3() { | |
| command pip3 "$@" | |
| local rc=$? | |
| if [ $rc -eq 0 ] && [ "${1:-}" = "install" ] \ | |
| && ! _hm_has_arg -r "${@:2}" && ! _hm_has_arg --requirement "${@:2}" \ | |
| && _hm_has_install_targets "${@:2}"; then | |
| _hm_append_cmd "pip install" "${@:2}" | |
| fi | |
| return $rc | |
| } | |
| uv() { | |
| command uv "$@" | |
| local rc=$? | |
| if [ $rc -eq 0 ] && [ "${1:-}" = "pip" ] && [ "${2:-}" = "install" ] \ | |
| && ! _hm_has_arg -r "${@:3}" && ! _hm_has_arg --requirements "${@:3}" \ | |
| && _hm_has_install_targets "${@:3}"; then | |
| _hm_append_cmd "uv pip install" "${@:3}" | |
| fi | |
| return $rc | |
| } | |
| npm() { | |
| command npm "$@" | |
| local rc=$? | |
| if [ $rc -eq 0 ] && { [ "${1:-}" = "install" ] || [ "${1:-}" = "i" ]; } && { [ "${2:-}" = "-g" ] || [ "${2:-}" = "--global" ]; } && _hm_has_install_targets "${@:3}"; then | |
| _hm_append_cmd "npm install -g" "${@:3}" | |
| fi | |
| return $rc | |
| } | |
| hermes() { | |
| command hermes "$@" | |
| local rc=$? | |
| if [ $rc -eq 0 ] && [ "${1:-}" = "plugins" ] && [ "${2:-}" = "install" ] && _hm_has_install_targets "${@:3}"; then | |
| _hm_append_cmd "hermes plugins install" "${@:3}" | |
| fi | |
| return $rc | |
| } | |
| BASHRC | |
| cat > "$HOME/.profile" << 'PROFILE' | |
| [ -n "${BASH_VERSION:-}" ] && [ -f ~/.bashrc ] && . ~/.bashrc | |
| PROFILE | |
| echo "Shell capture wrappers ready." | |
| # ββ Optional package installs from HF Variables/Secrets ββ | |
| HM_STARTUP_FAILURES=0 | |
| if [ -n "${HUGGINGMES_APT_PACKAGES:-}" ]; then | |
| echo "Installing apt packages from HUGGINGMES_APT_PACKAGES..." | |
| read -r -a HM_APT_PACKAGES <<< "${HUGGINGMES_APT_PACKAGES//,/ }" | |
| if command -v sudo >/dev/null 2>&1; then | |
| if sudo apt-get update && sudo apt-get install -y "${HM_APT_PACKAGES[@]}"; then | |
| echo "HUGGINGMES_APT_PACKAGES install complete." | |
| else | |
| HM_STARTUP_FAILURES=$((HM_STARTUP_FAILURES + 1)) | |
| echo "ERROR: HUGGINGMES_APT_PACKAGES install failed: ${HUGGINGMES_APT_PACKAGES}" >&2 | |
| fi | |
| elif [ "$(id -u)" -eq 0 ]; then | |
| if apt-get update && apt-get install -y "${HM_APT_PACKAGES[@]}"; then | |
| echo "HUGGINGMES_APT_PACKAGES install complete." | |
| else | |
| HM_STARTUP_FAILURES=$((HM_STARTUP_FAILURES + 1)) | |
| echo "ERROR: HUGGINGMES_APT_PACKAGES install failed: ${HUGGINGMES_APT_PACKAGES}" >&2 | |
| fi | |
| else | |
| HM_STARTUP_FAILURES=$((HM_STARTUP_FAILURES + 1)) | |
| echo "ERROR: root/sudo unavailable; HUGGINGMES_APT_PACKAGES skipped" >&2 | |
| fi | |
| fi | |
| if [ -n "${HUGGINGMES_PIP_PACKAGES:-}" ]; then | |
| echo "Installing Python packages from HUGGINGMES_PIP_PACKAGES..." | |
| read -r -a HM_PIP_PACKAGES <<< "${HUGGINGMES_PIP_PACKAGES//,/ }" | |
| if /opt/hermes/.venv/bin/pip install "${HM_PIP_PACKAGES[@]}"; then | |
| echo "HUGGINGMES_PIP_PACKAGES install complete." | |
| else | |
| HM_STARTUP_FAILURES=$((HM_STARTUP_FAILURES + 1)) | |
| echo "ERROR: HUGGINGMES_PIP_PACKAGES install failed: ${HUGGINGMES_PIP_PACKAGES}" >&2 | |
| fi | |
| fi | |
| if [ -n "${HUGGINGMES_NPM_PACKAGES:-}" ]; then | |
| echo "Installing npm packages from HUGGINGMES_NPM_PACKAGES..." | |
| read -r -a HM_NPM_PACKAGES <<< "${HUGGINGMES_NPM_PACKAGES//,/ }" | |
| if npm install -g "${HM_NPM_PACKAGES[@]}"; then | |
| echo "HUGGINGMES_NPM_PACKAGES install complete." | |
| else | |
| HM_STARTUP_FAILURES=$((HM_STARTUP_FAILURES + 1)) | |
| echo "ERROR: HUGGINGMES_NPM_PACKAGES install failed: ${HUGGINGMES_NPM_PACKAGES}" >&2 | |
| fi | |
| fi | |
| # ββ Arbitrary startup script (HUGGINGMES_RUN) ββ | |
| # Supports plain bash or base64-encoded scripts (prefix with base64: or b64:). | |
| # Example: HUGGINGMES_RUN="pip install pandas && npm install -g typescript" | |
| # Example: HUGGINGMES_RUN="base64:$(base64 -w0 setup.sh)" | |
| hm_run_startup_auto() { | |
| local payload="$1" | |
| [ -n "$payload" ] || return 0 | |
| local script_file | |
| script_file=$(mktemp "/tmp/huggingmes-startup.XXXXXX.sh") | |
| { | |
| echo 'export HUGGINGMES_CAPTURE_DISABLE=1' | |
| echo '[ -f ~/.bashrc ] && . ~/.bashrc' | |
| if [[ "$payload" == base64:* ]] || [[ "$payload" == b64:* ]]; then | |
| printf '%s' "${payload#*:}" | base64 -d | |
| else | |
| printf '%s\n' "$payload" | |
| fi | |
| } > "$script_file" | |
| chmod 700 "$script_file" | |
| echo "[startup:HUGGINGMES_RUN] running script" | |
| set +e | |
| bash "$script_file" | |
| local rc=$? | |
| set -e | |
| rm -f "$script_file" | |
| if [ $rc -eq 0 ]; then | |
| echo "[startup:HUGGINGMES_RUN] ok" | |
| else | |
| HM_STARTUP_FAILURES=$((HM_STARTUP_FAILURES + 1)) | |
| echo "ERROR: HUGGINGMES_RUN script failed (exit ${rc})" >&2 | |
| fi | |
| } | |
| if [ -n "${HUGGINGMES_RUN:-}" ]; then | |
| hm_run_startup_auto "$HUGGINGMES_RUN" | |
| fi | |
| # ββ Run workspace startup script ββ | |
| # Replays install commands recorded by the shell wrappers from previous sessions. | |
| if [ -s "$STARTUP_FILE" ]; then | |
| echo "Running workspace/startup.sh..." | |
| set +e | |
| HUGGINGMES_CAPTURE_DISABLE=1 bash -l "$STARTUP_FILE" | |
| set -e | |
| echo "Workspace startup script complete." | |
| fi | |
| if [ "$HM_STARTUP_FAILURES" -gt 0 ]; then | |
| echo "Warning: ${HM_STARTUP_FAILURES} startup step(s) failed. Check logs above." >&2 | |
| fi | |
| # ββ Start background services ββ | |
| node "$APP_DIR/health-server.js" & | |
| HEALTH_PID=$! | |
| if [ -n "${WEBHOOK_URL:-}" ]; then | |
| python3 - <<'PY' >/dev/null 2>&1 & | |
| import json, os, urllib.request | |
| body = json.dumps({ | |
| "event": "restart", | |
| "status": "success", | |
| "message": "HuggingMes Hermes gateway has started.", | |
| "model": os.environ.get("MODEL_FOR_CONFIG", ""), | |
| }).encode() | |
| req = urllib.request.Request(os.environ["WEBHOOK_URL"], data=body, method="POST", headers={"Content-Type": "application/json"}) | |
| urllib.request.urlopen(req, timeout=10).read() | |
| PY | |
| fi | |
| # ββ Launch dashboard once (restarts if it dies) ββ | |
| start_dashboard_once() { | |
| if [ -n "${DASHBOARD_PID:-}" ] && kill -0 "$DASHBOARD_PID" 2>/dev/null; then | |
| return 0 | |
| fi | |
| echo "Launching Hermes dashboard on 127.0.0.1:${DASHBOARD_PORT}..." | |
| (hermes dashboard --host 127.0.0.1 --insecure 2>&1 | tee -a "$HERMES_HOME/logs/dashboard.log") & | |
| DASHBOARD_PID=$! | |
| } | |
| # ββ Start sync loop once β survives gateway restarts ββ | |
| start_background_sync_once() { | |
| [ -n "${HF_TOKEN:-}" ] || return 0 | |
| if [ -n "${SYNC_LOOP_PID:-}" ] && kill -0 "$SYNC_LOOP_PID" 2>/dev/null; then | |
| return 0 | |
| fi | |
| python3 -u "$APP_DIR/hermes-sync.py" loop & | |
| SYNC_LOOP_PID=$! | |
| } | |
| start_mimo_proxy_once | |
| start_dashboard_once | |
| start_jupyter | |
| start_ssh_once | |
| start_cloudflare_tunnel_once | |
| # ββ Gateway restart loop ββ | |
| GATEWAY_RESTART_DELAY="${GATEWAY_RESTART_DELAY:-5}" | |
| GATEWAY_MAX_RESTARTS="${GATEWAY_MAX_RESTARTS:-0}" | |
| GATEWAY_RESTART_COUNT=0 | |
| GATEWAY_READY_TIMEOUT="${GATEWAY_READY_TIMEOUT:-120}" | |
| while true; do | |
| # Monitor health-server β restart if it died unexpectedly | |
| if [ -n "${HEALTH_PID:-}" ] && ! kill -0 "$HEALTH_PID" 2>/dev/null; then | |
| echo "Warning: health-server exited (PID $HEALTH_PID dead); restarting..." | |
| node "$APP_DIR/health-server.js" & | |
| HEALTH_PID=$! | |
| echo "Health server restarted (PID: $HEALTH_PID)" | |
| fi | |
| # Monitor Hermes dashboard β restart if it died unexpectedly | |
| if [ -n "${DASHBOARD_PID:-}" ] && ! kill -0 "$DASHBOARD_PID" 2>/dev/null; then | |
| echo "Warning: Hermes dashboard exited; restarting..." | |
| start_dashboard_once | |
| fi | |
| # Monitor JupyterLab β restart if it died unexpectedly | |
| if [ "${DEV_MODE:-true}" != "false" ] && [ -n "${JUPYTER_PID:-}" ] && ! kill -0 "$JUPYTER_PID" 2>/dev/null; then | |
| echo "Warning: JupyterLab exited (PID $JUPYTER_PID dead); restarting..." | |
| unset JUPYTER_PID | |
| start_jupyter | |
| fi | |
| if [ "${MIMO_PROXY_ACTIVE:-false}" = "true" ] && { [ -z "${MIMO_PROXY_PID:-}" ] || ! kill -0 "$MIMO_PROXY_PID" 2>/dev/null; }; then | |
| echo "Warning: MiMo proxy is not running; restarting..." | |
| unset MIMO_PROXY_PID | |
| start_mimo_proxy_once | |
| fi | |
| if [ -n "${SSH_PUBLIC_KEY:-${SSH_AUTHORIZED_KEYS:-}}" ] && { [ -z "${SSHD_PID:-}" ] || ! kill -0 "$SSHD_PID" 2>/dev/null; }; then | |
| echo "Warning: SSH service is not running; restarting..." | |
| unset SSHD_PID | |
| start_ssh_once | |
| fi | |
| CLOUDFLARE_TUNNEL_WATCH_MODE="$(printf '%s' "${CLOUDFLARE_TUNNEL_ENABLED:-auto}" | tr '[:upper:]' '[:lower:]')" | |
| if [ -n "${CLOUDFLARE_TUNNEL_TOKEN:-${CF_TUNNEL_TOKEN:-}}" ] \ | |
| && [ "$CLOUDFLARE_TUNNEL_WATCH_MODE" != "false" ] \ | |
| && [ "$CLOUDFLARE_TUNNEL_WATCH_MODE" != "0" ] \ | |
| && [ "$CLOUDFLARE_TUNNEL_WATCH_MODE" != "no" ] \ | |
| && [ "$CLOUDFLARE_TUNNEL_WATCH_MODE" != "off" ] \ | |
| && { [ -z "${CLOUDFLARED_PID:-}" ] || ! kill -0 "$CLOUDFLARED_PID" 2>/dev/null; }; then | |
| echo "Warning: Cloudflare Tunnel is not running; restarting..." | |
| unset CLOUDFLARED_PID | |
| start_cloudflare_tunnel_once | |
| fi | |
| echo "Launching Hermes gateway..." | |
| (hermes gateway run --no-supervise 2>&1 | tee -a "$HERMES_HOME/logs/gateway.log") & | |
| GATEWAY_PID=$! | |
| ready=false | |
| for ((i=0; i<GATEWAY_READY_TIMEOUT; i++)); do | |
| if (echo > "/dev/tcp/127.0.0.1/${GATEWAY_API_PORT}") 2>/dev/null; then | |
| ready=true | |
| break | |
| fi | |
| if ! kill -0 "$GATEWAY_PID" 2>/dev/null; then | |
| break | |
| fi | |
| sleep 1 | |
| done | |
| if [ "$ready" != "true" ]; then | |
| echo "" | |
| echo "Hermes gateway failed to expose the API health port. Last 40 log lines:" | |
| echo "----------------------------------------" | |
| tail -40 "$HERMES_HOME/logs/gateway.log" || true | |
| exit 1 | |
| fi | |
| # Start sync loop (only once β shared across all gateway restarts) | |
| start_background_sync_once | |
| set +e | |
| wait "$GATEWAY_PID" | |
| GATEWAY_EXIT_CODE=$? | |
| set -e | |
| # Sync state before restart | |
| if [ -n "${HF_TOKEN:-}" ]; then | |
| echo "Gateway exited β syncing state before restart..." | |
| python3 "$APP_DIR/hermes-sync.py" sync-once || echo "Warning: sync failed." | |
| fi | |
| GATEWAY_RESTART_COUNT=$((GATEWAY_RESTART_COUNT + 1)) | |
| if [ "$GATEWAY_MAX_RESTARTS" != "0" ] && [ "$GATEWAY_RESTART_COUNT" -ge "$GATEWAY_MAX_RESTARTS" ]; then | |
| echo "Gateway exited (code ${GATEWAY_EXIT_CODE}); restart limit (${GATEWAY_MAX_RESTARTS}) reached." | |
| exit "$GATEWAY_EXIT_CODE" | |
| fi | |
| echo "Gateway exited (code ${GATEWAY_EXIT_CODE}); restarting in ${GATEWAY_RESTART_DELAY}s..." | |
| sleep "$GATEWAY_RESTART_DELAY" | |
| done | |