#!/bin/bash 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" <&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"