diff --git a/setup_timmy.sh b/setup_timmy.sh index 4b2f50ee..8aeed4a4 100755 --- a/setup_timmy.sh +++ b/setup_timmy.sh @@ -1,21 +1,34 @@ #!/usr/bin/env bash # ============================================================================= -# Sovereign Agent Stack — VPS Deployment Script v7 +# Sovereign Agent Stack — VPS Deployment Script v8 # -# Paperclip only. No fluff. +# Hermes Agent + Paperclip, Tailscale-only. +# - Hermes Agent (Nous Research) — persistent AI agent # - Paperclip in local_trusted mode (127.0.0.1:3100) -# - Nginx reverse proxy on port 80 -# - Cookie-based auth gate — login once, 7-day session +# - Nginx reverse proxy on Tailscale IP (port 80) +# - Cookie-based auth gate for Paperclip # - PostgreSQL backend +# - UFW locked to Tailscale + SSH only +# - systemd services for auto-restart +# - Daily backups with 30-day retention # # Usage: -# curl -O https://raw.githubusercontent.com/AlexanderWhitestone/Timmy-time-dashboard/main/setup_timmy.sh -# chmod +x setup_timmy.sh -# ./setup_timmy.sh install -# ./setup_timmy.sh start +# ./setup_timmy.sh install # Full install (run once on fresh VPS) +# ./setup_timmy.sh start # Start all services +# ./setup_timmy.sh stop # Stop all services +# ./setup_timmy.sh restart # Stop + start +# ./setup_timmy.sh status # Health check +# ./setup_timmy.sh logs # Tail all logs # -# Dashboard: http://YOUR_DOMAIN (behind auth) +# Prerequisites: +# - Ubuntu 22.04+ VPS with root access +# - Tailscale installed and joined to tailnet +# - SSH key added +# +# Access (Tailscale only): +# Paperclip: http:// +# Hermes: ssh to VPS, then `hermes` # ============================================================================= set -euo pipefail @@ -25,8 +38,10 @@ PROJECT_DIR="${PROJECT_DIR:-$HOME/sovereign-stack}" PAPERCLIP_DIR="$PROJECT_DIR/paperclip" LOG_DIR="$PROJECT_DIR/logs" PID_DIR="$PROJECT_DIR/pids" +BACKUP_DIR="$HOME/backups/hermes" -DOMAIN="${DOMAIN:-$(curl -s ifconfig.me)}" +# Tailscale IP (auto-detected) +TSIP="${TSIP:-$(tailscale ip -4 2>/dev/null || echo '127.0.0.1')}" DB_USER="paperclip" DB_PASS="paperclip" @@ -47,51 +62,53 @@ warn() { echo -e "${YELLOW}⚠${NC} $1"; } fail() { echo -e "${RED}✘${NC} $1"; exit 1; } info() { echo -e "${BOLD}$1${NC}"; } -# --- Secrets --- -load_or_create_secrets() { - if [ -f "$SECRETS_FILE" ]; then - source "$SECRETS_FILE" - step "Loaded secrets" - else - BETTER_AUTH_SECRET="sovereign-$(openssl rand -hex 16)" - PAPERCLIP_AGENT_JWT_SECRET="agent-$(openssl rand -hex 16)" - cat > "$SECRETS_FILE" </dev/null 2>&1; then - step "Installing dependencies..." - sudo apt-get update -y > /dev/null 2>&1 - sudo apt-get install -y curl git postgresql postgresql-contrib build-essential nginx > /dev/null 2>&1 + step "Installing system packages..." + apt-get update -y > /dev/null 2>&1 + apt-get install -y curl git postgresql postgresql-contrib build-essential nginx lsof > /dev/null 2>&1 fi + # Node.js 20 if ! command -v node >/dev/null 2>&1; then step "Installing Node.js 20..." - curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - > /dev/null 2>&1 - sudo apt-get install -y nodejs > /dev/null 2>&1 + curl -fsSL https://deb.nodesource.com/setup_20.x | bash - > /dev/null 2>&1 + apt-get install -y nodejs > /dev/null 2>&1 fi step "Node $(node -v)" + # pnpm if ! command -v pnpm >/dev/null 2>&1; then step "Installing pnpm..." - sudo npm install -g pnpm > /dev/null 2>&1 + npm install -g pnpm > /dev/null 2>&1 fi step "pnpm $(pnpm -v)" + + # uv (for Hermes) + if ! command -v uv >/dev/null 2>&1 && [ ! -f "$HOME/.local/bin/uv" ]; then + step "Installing uv..." + curl -LsSf https://astral.sh/uv/install.sh | sh > /dev/null 2>&1 + fi + export PATH="$HOME/.local/bin:$PATH" + step "uv ready" } # --- Database --- setup_database() { banner "Database" - sudo systemctl start postgresql || sudo service postgresql start || true + systemctl start postgresql || service postgresql start || true sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='$DB_USER'" | grep -q 1 || \ sudo -u postgres psql -c "CREATE USER $DB_USER WITH PASSWORD '$DB_PASS' SUPERUSER;" @@ -102,6 +119,43 @@ setup_database() { step "Database ready" } +# --- Hermes Agent --- +install_hermes() { + banner "Hermes Agent" + + if [ -d "$HOME/.hermes/hermes-agent" ]; then + step "Hermes already cloned, updating..." + cd "$HOME/.hermes/hermes-agent" && git pull origin main 2>/dev/null || true + cd - > /dev/null + else + step "Installing Hermes (one-liner)..." + curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash -s -- --skip-setup + fi + + # Ensure venv exists + export PATH="$HOME/.local/bin:$HOME/.hermes/node/bin:$PATH" + if [ ! -d "$HOME/.hermes/hermes-agent/.venv" ]; then + step "Creating Hermes venv..." + cd "$HOME/.hermes/hermes-agent" + uv venv .venv --python 3.12 2>/dev/null || uv venv .venv + source .venv/bin/activate + uv pip install -e ".[all]" 2>&1 | tail -3 + cd - > /dev/null + fi + + # Add hermes to PATH in bashrc if not already there + if ! grep -q 'hermes-agent/.venv/bin' "$HOME/.bashrc" 2>/dev/null; then + cat >> "$HOME/.bashrc" <<'BASHRC' + +# Sovereign Stack — Hermes Agent +export PATH="$HOME/.local/bin:$HOME/.hermes/node/bin:$HOME/.hermes/hermes-agent/.venv/bin:$PATH" +alias hermes='cd ~/.hermes/hermes-agent && source .venv/bin/activate && python hermes' +BASHRC + fi + + step "Hermes installed" +} + # --- Auth Gate --- install_auth_gate() { step "Installing auth gate..." @@ -158,14 +212,13 @@ AUTHGATE chmod +x "$PROJECT_DIR/auth-gate.py" } -# --- Nginx --- +# --- Nginx (Tailscale-only) --- install_nginx() { - step "Configuring nginx..." + step "Configuring nginx (Tailscale-only: $TSIP)..." cat > /etc/nginx/sites-available/paperclip < "$SECRETS_FILE" < /etc/systemd/system/sovereign-auth-gate.service < /etc/systemd/system/sovereign-paperclip.service < /etc/systemd/system/hermes-gateway.service < /etc/systemd/system/sovereign-paperclip.service.d/secrets.conf < "$HOME/scripts/hermes-backup.sh" <<'BACKUP' +#!/bin/bash +BACKUP_DIR=$HOME/backups/hermes +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +HERMES_HOME=$HOME/.hermes + +mkdir -p $BACKUP_DIR + +tar czf $BACKUP_DIR/hermes_$TIMESTAMP.tar.gz \ + $HERMES_HOME/config.yaml \ + $HERMES_HOME/.env \ + $HERMES_HOME/hermes-agent/skills/ \ + $HERMES_HOME/hermes-agent/sessions/ \ + $HERMES_HOME/cron/ \ + 2>/dev/null || true + +# Prune backups older than 30 days +find $BACKUP_DIR -name 'hermes_*.tar.gz' -mtime +30 -delete + +echo "$(date): Backup complete: hermes_$TIMESTAMP.tar.gz" +BACKUP + mkdir -p "$HOME/scripts" + chmod +x "$HOME/scripts/hermes-backup.sh" + + # Cron: daily at 3 AM + (crontab -l 2>/dev/null | grep -v hermes-backup; echo "0 3 * * * $HOME/scripts/hermes-backup.sh >> /var/log/hermes-backup.log 2>&1") | crontab - + + step "Daily backup at 3 AM, 30-day retention" +} + +# ============================================================================= +# RUNTIME +# ============================================================================= + kill_zombies() { step "Cleaning stale processes..." - for port in $(seq 3100 3110); do + for port in 3100 3101 3102 3103 3104 3105 9876; do pid=$(lsof -ti :$port 2>/dev/null || true) [ -n "$pid" ] && kill -9 $pid 2>/dev/null && step "Killed port $port" || true done - pid=$(lsof -ti :9876 2>/dev/null || true) - [ -n "$pid" ] && kill -9 $pid 2>/dev/null || true sleep 1 } -# --- Start --- start_services() { banner "Starting" load_or_create_secrets kill_zombies - # Stop Docker Caddy if conflicting on port 80 - if docker ps --format '{{.Names}}' 2>/dev/null | grep -q timmy-caddy; then - step "Stopping Docker Caddy (port 80)..." - docker stop timmy-caddy > /dev/null 2>&1 || true - fi - - step "Auth gate..." - AUTH_USER="$AUTH_USER" AUTH_PASS="$AUTH_PASS" \ - nohup python3 "$PROJECT_DIR/auth-gate.py" > "$LOG_DIR/auth-gate.log" 2>&1 & - echo $! > "$PID_DIR/auth-gate.pid" - - step "Nginx..." + systemctl restart sovereign-auth-gate systemctl restart nginx - - step "Paperclip (local_trusted → 127.0.0.1:3100)..." - cd "$PAPERCLIP_DIR" - HOST=127.0.0.1 \ - DATABASE_URL="$DATABASE_URL" \ - PAPERCLIP_DEPLOYMENT_MODE=local_trusted \ - BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET" \ - PAPERCLIP_AGENT_JWT_SECRET="$PAPERCLIP_AGENT_JWT_SECRET" \ - BETTER_AUTH_URL="http://$DOMAIN" \ - nohup pnpm dev > "$LOG_DIR/paperclip.log" 2>&1 & - echo $! > "$PID_DIR/paperclip.pid" - cd - > /dev/null + systemctl restart sovereign-paperclip step "Waiting for Paperclip..." for i in $(seq 1 30); do - if grep -q "Server listening on" "$LOG_DIR/paperclip.log" 2>/dev/null; then - echo "" - info "══════════════════════════════════════" - info " Dashboard: http://$DOMAIN" - info " Auth: $AUTH_USER / ********" - info "══════════════════════════════════════" - return + if systemctl is-active --quiet sovereign-paperclip && curl -s -o /dev/null http://127.0.0.1:3100 2>/dev/null; then + break fi printf "."; sleep 2 done - echo ""; warn "Slow start. Check: tail -f $LOG_DIR/paperclip.log" + echo "" + + info "══════════════════════════════════════" + info " Paperclip: http://$TSIP" + info " Hermes: ssh root@$TSIP → hermes" + info " Auth: $AUTH_USER / ********" + info "══════════════════════════════════════" } -# --- Stop --- stop_services() { banner "Stopping" - for service in paperclip auth-gate; do - pid_file="$PID_DIR/$service.pid" - if [ -f "$pid_file" ]; then - pid=$(cat "$pid_file") - ps -p "$pid" > /dev/null 2>&1 && kill "$pid" 2>/dev/null && step "Stopped $service" || true - rm -f "$pid_file" - fi - done + systemctl stop sovereign-paperclip 2>/dev/null || true + systemctl stop sovereign-auth-gate 2>/dev/null || true + systemctl stop hermes-gateway 2>/dev/null || true + step "All services stopped" } -# --- Status --- show_status() { banner "Status" - for s in paperclip auth-gate; do - pf="$PID_DIR/$s.pid" - if [ -f "$pf" ] && ps -p $(cat "$pf") > /dev/null 2>&1; then - echo -e " ${GREEN}●${NC} $s" + for svc in sovereign-auth-gate sovereign-paperclip hermes-gateway nginx postgresql; do + if systemctl is-active --quiet $svc 2>/dev/null; then + echo -e " ${GREEN}●${NC} $svc" else - echo -e " ${RED}○${NC} $s" + echo -e " ${RED}○${NC} $svc" fi done echo "" + echo -e " Tailscale: $TSIP" + echo -e " Paperclip: http://$TSIP" + echo "" for port in 80 3100 9876; do if lsof -ti :$port > /dev/null 2>&1; then echo -e " ${GREEN}●${NC} :$port" @@ -321,17 +490,32 @@ show_status() { echo -e " ${RED}○${NC} :$port" fi done - echo "" - systemctl is-active --quiet nginx && echo -e " ${GREEN}●${NC} nginx" || echo -e " ${RED}○${NC} nginx" +} + +# ============================================================================= +# FULL INSTALL (run once) +# ============================================================================= +full_install() { + check_preflight + setup_database + install_hermes + install_paperclip + load_or_create_secrets + install_systemd + install_backups + setup_firewall + + banner "Install Complete" + info "Run: ./setup_timmy.sh start" } # --- CLI --- case "${1:-}" in - install) check_preflight; setup_database; install_stack ;; + install) full_install ;; start) start_services ;; stop) stop_services ;; restart) stop_services; sleep 2; start_services ;; status) show_status ;; - logs) tail -f "$LOG_DIR"/*.log ;; + logs) journalctl -u sovereign-paperclip -u sovereign-auth-gate -u hermes-gateway -f ;; *) echo "Usage: $0 {install|start|stop|restart|status|logs}"; exit 1 ;; esac