Spaces:
Paused
Paused
| set -e | |
| BOOT_START=$(date +%s) | |
| echo "[entrypoint] Hermes Agent on HuggingFace Spaces (Safe Sync Mode)" | |
| echo "===============================================================================" | |
| # ββ Configuration βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| HERMES_WORK="/opt/data" # Fast local filesystem (working dir) | |
| HERMES_BACKUP="/data/hermes" # HF mounted bucket (backup only) | |
| INSTALL_DIR="/opt/hermes" | |
| HERMES_UID=1000 | |
| HERMES_GID=1000 | |
| SYNC_INTERVAL="${SYNC_INTERVAL:-60}" # Default: sync every 1 minute | |
| # ββ Singleton guard ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Hanya satu instance yang menjalankan steps. Instance duplikat (dari HF rapid- | |
| # restart) cukup sleep infinity β tidak exit (exit = HF restart lagi). | |
| # HERMES_ENTRYPOINT_OWNER di-export sebelum exec gosu sehingga instance setelah | |
| # privilege-drop tahu dia adalah kelanjutan sah, bukan duplikat. | |
| ENTRYPOINT_PID_FILE="/tmp/hermes-entrypoint.pid" | |
| ENTRYPOINT_LOG="/tmp/hermes-entrypoint.log" | |
| if [ -z "${HERMES_ENTRYPOINT_OWNER:-}" ]; then | |
| # Instance baru tanpa token β cek apakah ada instance lain yang sudah running | |
| if [ -f "$ENTRYPOINT_PID_FILE" ]; then | |
| OLD_PID=$(cat "$ENTRYPOINT_PID_FILE" 2>/dev/null || echo "") | |
| if [ -n "$OLD_PID" ] && kill -0 "$OLD_PID" 2>/dev/null; then | |
| echo "[entrypoint] Instance $OLD_PID already running β standing by silently." | |
| sleep infinity | |
| exit 0 | |
| else | |
| echo "[entrypoint] Stale PID $OLD_PID (dead) β taking over..." | |
| rm -f "$ENTRYPOINT_PID_FILE" "$ENTRYPOINT_LOG" 2>/dev/null || true | |
| fi | |
| fi | |
| echo $$ > "$ENTRYPOINT_PID_FILE" | |
| export HERMES_ENTRYPOINT_OWNER=$$ | |
| # Buat log world-writable sekarang (sebagai root) agar hermes bisa write setelah gosu | |
| touch "$ENTRYPOINT_LOG" && chmod 666 "$ENTRYPOINT_LOG" | |
| fi | |
| exec > >(tee -a "$ENTRYPOINT_LOG") 2>&1 | |
| # ββ Helper: Safe Sync (EXCLUDE large/temp files) βββββββββββββββββββββββββββ | |
| safe_sync() { | |
| local SRC="$1" | |
| local DST="$2" | |
| if [ ! -d "$SRC" ]; then | |
| echo " Source $SRC does not exist, skipping sync" | |
| return 0 | |
| fi | |
| mkdir -p "$DST" | |
| echo " Syncing: $SRC -> $DST" | |
| echo " Excluding: node_modules, .cache, logs, *.tmp, *.lock, *.pid, .local/share/uv" | |
| rsync -a --checksum --delete \ | |
| --no-owner --no-group \ | |
| --exclude='.local/share/uv' \ | |
| --exclude='node_modules' \ | |
| --exclude='.cache' \ | |
| --exclude='.playwright' \ | |
| --exclude='logs' \ | |
| --exclude='*.log' \ | |
| --exclude='*.log.*' \ | |
| --exclude='*.tmp' \ | |
| --exclude='*.lock' \ | |
| --exclude='*.pid' \ | |
| --exclude='__pycache__' \ | |
| --exclude='state.db-wal' \ | |
| --exclude='state.db-shm' \ | |
| "$SRC/" "$DST/" | |
| echo " β Sync completed" | |
| } | |
| # ββ Stage 1: Restore from backup (as root, before privilege drop) ββββββββββ | |
| if [ "$(id -u)" = "0" ]; then | |
| echo "" | |
| echo "=== 1. Restoring Hermes data from persistent storage ===" | |
| # Create working directory on fast local filesystem | |
| mkdir -p "$HERMES_WORK"/{cron,sessions,logs,hooks,memories,skills,skins,plans,workspace,home} | |
| mkdir -p "$HERMES_BACKUP"/{cron,sessions,logs,hooks,memories,skills,skins,plans,workspace,home} | |
| chown -R ${HERMES_UID}:${HERMES_GID} "$HERMES_BACKUP" | |
| # Restore from backup if exists | |
| if [ -d "$HERMES_BACKUP" ] && [ "$(ls -A $HERMES_BACKUP 2>/dev/null | wc -l)" -gt 0 ]; then | |
| echo "Found existing data in $HERMES_BACKUP" | |
| RESTORE_START=$(date +%s) | |
| safe_sync "$HERMES_BACKUP" "$HERMES_WORK" | |
| RESTORE_END=$(date +%s) | |
| echo "[TIMER] Restore from backup: $((RESTORE_END - RESTORE_START))s" | |
| echo "Restore completed (sessions, memories, workspace restored)" | |
| else | |
| echo "No existing backup found, starting fresh" | |
| fi | |
| # Fix ownership on working directory (fast - only local files) | |
| chown -R ${HERMES_UID}:${HERMES_GID} "$HERMES_WORK" | |
| echo "β Working directory ready: $HERMES_WORK (owned by hermes)" | |
| # Ensure /tmp is world-writable | |
| chmod 1777 /tmp 2>/dev/null || true | |
| echo "" | |
| echo "=== 2. Starting Scheduled Backup (Every ${SYNC_INTERVAL}s) ===" | |
| # Background backup loop β uses safe_sync which already excludes .local/share/uv | |
| (while true; do | |
| sleep ${SYNC_INTERVAL} | |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backing up $HERMES_WORK -> $HERMES_BACKUP" | |
| safe_sync "$HERMES_WORK" "$HERMES_BACKUP" | |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup completed" | |
| done) & | |
| BACKUP_PID=$! | |
| echo "Backup loop started with PID $BACKUP_PID" | |
| echo "" | |
| echo "=== 3. Starting nginx reverse proxy (port 7860 β 7861) ===" | |
| nginx | |
| echo " β nginx started (PID $(cat /run/nginx.pid 2>/dev/null || echo unknown))" | |
| echo "" | |
| echo "=== 4. Dropping privileges to hermes (uid ${HERMES_UID}) ===" | |
| exec gosu ${HERMES_UID} "$0" "$@" | |
| fi | |
| # ββ Stage 2: Run as hermes user βββββββββββββββββββββββββββββββββββββββββββββ | |
| echo "" | |
| echo "=== 4. Activating Python virtual environment ===" | |
| if [ -f "${INSTALL_DIR}/.venv/bin/activate" ]; then | |
| source "${INSTALL_DIR}/.venv/bin/activate" | |
| echo "β Activated venv: $(which python3)" | |
| fi | |
| # ββ Bootstrap config files (in working dir, not mounted bucket) βββββββββββββ | |
| echo "" | |
| echo "=== 5. Bootstrapping configuration files ===" | |
| if [ ! -f "$HERMES_WORK/.env" ] && [ -f "$INSTALL_DIR/.env.example" ]; then | |
| cp "$INSTALL_DIR/.env.example" "$HERMES_WORK/.env" | |
| echo "β Created .env from example" | |
| fi | |
| if [ ! -f "$HERMES_WORK/config.yaml" ] && [ -f "$INSTALL_DIR/cli-config.yaml.example" ]; then | |
| cp "$INSTALL_DIR/cli-config.yaml.example" "$HERMES_WORK/config.yaml" | |
| echo "β Created config.yaml from example" | |
| fi | |
| if [ ! -f "$HERMES_WORK/SOUL.md" ] && [ -f "$INSTALL_DIR/docker/SOUL.md" ]; then | |
| cp "$INSTALL_DIR/docker/SOUL.md" "$HERMES_WORK/SOUL.md" | |
| echo "β Created SOUL.md from template" | |
| fi | |
| # ββ Dashboard basic auth ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| DASH_USER="${HERMES_DASHBOARD_USERNAME:-admin}" | |
| DASH_PASS="${HERMES_DASHBOARD_PASSWORD:-}" | |
| if [ -n "$DASH_PASS" ] && [ -f "$HERMES_WORK/config.yaml" ]; then | |
| if ! grep -q 'basic_auth' "$HERMES_WORK/config.yaml"; then | |
| cat >> "$HERMES_WORK/config.yaml" <<EOF | |
| dashboard: | |
| basic_auth: | |
| username: ${DASH_USER} | |
| password: ${DASH_PASS} | |
| EOF | |
| echo "β Injected dashboard.basic_auth into config.yaml" | |
| fi | |
| fi | |
| if [ -n "$DASH_PASS" ]; then | |
| export HERMES_DASHBOARD_BASIC_AUTH_USERNAME="$DASH_USER" | |
| export HERMES_DASHBOARD_BASIC_AUTH_PASSWORD="$DASH_PASS" | |
| fi | |
| # ββ Sync bundled skills βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if [ -d "$INSTALL_DIR/skills" ] && [ -f "$INSTALL_DIR/tools/skills_sync.py" ]; then | |
| python3 "$INSTALL_DIR/tools/skills_sync.py" 2>&1 || echo "[entrypoint] Skills sync skipped" | |
| fi | |
| # ββ Build artifacts check βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| echo "" | |
| echo "=== 6. Build artifacts check ===" | |
| test -f "$INSTALL_DIR/run_agent.py" && echo " β run_agent.py" || echo " β run_agent.py not found" | |
| test -f "$INSTALL_DIR/gateway/run.py" && echo " β gateway/run.py" || echo " β gateway/run.py not found" | |
| test -d "$INSTALL_DIR/web" && echo " β web/ dashboard" || echo " β web/ not found" | |
| command -v hermes >/dev/null 2>&1 && echo " β hermes CLI: $(which hermes)" || echo " β hermes CLI not in PATH" | |
| # ββ Register Buzz profile βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if [ -n "${BUZZ_RELAY_URL:-}" ] && [ -n "${BUZZ_PRIVATE_KEY:-}" ]; then | |
| _BUZZ_NAME="${BUZZ_DISPLAY_NAME:-${AGENT_NAME:-$(hostname)}}" | |
| echo "" | |
| echo "=== 7. Setting Buzz profile name: ${_BUZZ_NAME} ===" | |
| buzz users set-profile --name "${_BUZZ_NAME}" 2>&1 || \ | |
| echo "β buzz users set-profile failed (non-fatal)" | |
| fi | |
| # ββ Export working directory as HERMES_HOME βββββββββββββββββββββββββββββββββ | |
| export HERMES_HOME="$HERMES_WORK" | |
| echo "" | |
| echo "=== 8. Environment Setup ===" | |
| echo " Working directory (FAST): $HERMES_WORK" | |
| echo " Backup directory (HF Bucket): $HERMES_BACKUP" | |
| echo " Sync interval: ${SYNC_INTERVAL}s" | |
| echo " HERMES_HOME: $HERMES_HOME" | |
| ENTRYPOINT_END=$(date +%s) | |
| echo "" | |
| echo "[TIMER] Entrypoint setup: $((ENTRYPOINT_END - BOOT_START))s" | |
| # ββ Start Hermes Gateway ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| echo "" | |
| echo "=== 9. Starting Hermes Agent ===" | |
| echo " Hermes running on local filesystem: $HERMES_WORK (FAST)" | |
| echo " Persistent storage: $HERMES_BACKUP (synced every ${SYNC_INTERVAL}s)" | |
| echo "" | |
| cd "$HERMES_WORK" | |
| # Trap SIGTERM untuk final backup sebelum shutdown (dipasang di Stage 2 karena exec gosu menggantikan Stage 1) | |
| trap "echo ''; echo 'Shutting down... performing final backup'; safe_sync '$HERMES_WORK' '$HERMES_BACKUP'; echo 'Final backup completed'; exit 0" SIGTERM SIGINT | |
| # # Initial backup segera setelah start (jangan tunggu interval pertama) | |
| # (sleep 30 && echo "[$(date '+%Y-%m-%d %H:%M:%S')] Initial post-start backup" && safe_sync "$HERMES_WORK" "$HERMES_BACKUP" && echo "[$(date '+%Y-%m-%d %H:%M:%S')] Initial backup done") & | |
| # Start dashboard di background (port 7861 untuk HF Spaces) | |
| "${INSTALL_DIR}/.venv/bin/hermes" dashboard --host 0.0.0.0 --port 7861 --no-open --insecure & | |
| DASHBOARD_PID=$! | |
| echo " Dashboard started (PID $DASHBOARD_PID) on :7860" | |
| sleep 2 | |
| exec "${INSTALL_DIR}/.venv/bin/python3" -u "${INSTALL_DIR}/gateway/run.py" | |