#!/usr/bin/env bash # ============================================================================= # Sovereign Agent Stack — VPS Deployment Script v8 # # 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 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: # ./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 # # 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 # --- Configuration --- 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" # Tailscale IP (auto-detected) TSIP="${TSIP:-$(tailscale ip -4 2>/dev/null || echo '127.0.0.1')}" DB_USER="paperclip" DB_PASS="paperclip" DB_NAME="paperclip" DATABASE_URL="postgresql://$DB_USER:$DB_PASS@localhost:5432/$DB_NAME" AUTH_USER="${AUTH_USER:-Rockachopa}" AUTH_PASS="${AUTH_PASS:-Iamrockachopathegend}" SECRETS_FILE="$PROJECT_DIR/.secrets" # --- Colors --- CYAN='\033[0;36m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; BOLD='\033[1m'; NC='\033[0m' banner() { echo -e "\n${CYAN}═══════════════════════════════════════════════${NC}\n${BOLD}$1${NC}\n${CYAN}═══════════════════════════════════════════════${NC}\n"; } step() { echo -e "${GREEN}▸${NC} $1"; } warn() { echo -e "${YELLOW}⚠${NC} $1"; } fail() { echo -e "${RED}✘${NC} $1"; exit 1; } info() { echo -e "${BOLD}$1${NC}"; } # ============================================================================= # INSTALL # ============================================================================= check_preflight() { banner "Preflight" # Tailscale must be connected if [ "$TSIP" = "127.0.0.1" ]; then fail "Tailscale not connected. Run: tailscale up --authkey=YOUR_KEY" fi step "Tailscale IP: $TSIP" if command -v apt-get >/dev/null 2>&1; then 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 | 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..." 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" 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;" sudo -u postgres psql -lqt | cut -d \| -f 1 | grep -qw "$DB_NAME" || \ sudo -u postgres psql -c "CREATE DATABASE $DB_NAME OWNER $DB_USER;" 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..." cat > "$PROJECT_DIR/auth-gate.py" <<'AUTHGATE' #!/usr/bin/env python3 """Cookie-based auth gate. Login once, 7-day session.""" import hashlib, hmac, http.server, time, base64, os SECRET = os.environ.get("AUTH_GATE_SECRET", "sovereign-timmy-gate-2026") USER = os.environ.get("AUTH_USER", "admin") PASS = os.environ.get("AUTH_PASS", "changeme") COOKIE_NAME = "sovereign_gate" COOKIE_MAX_AGE = 86400 * 7 def make_token(ts): return hmac.new(SECRET.encode(), f"{USER}:{ts}".encode(), hashlib.sha256).hexdigest()[:32] def verify_token(token): try: parts = token.split(".") if len(parts) != 2: return False ts, sig = int(parts[0]), parts[1] if time.time() - ts > COOKIE_MAX_AGE: return False return sig == make_token(ts) except: return False class Handler(http.server.BaseHTTPRequestHandler): def log_message(self, *a): pass def do_GET(self): for c in self.headers.get("Cookie", "").split(";"): c = c.strip() if c.startswith(f"{COOKIE_NAME}=") and verify_token(c[len(COOKIE_NAME)+1:]): self.send_response(200); self.end_headers(); return auth = self.headers.get("Authorization", "") if auth.startswith("Basic "): try: u, p = base64.b64decode(auth[6:]).decode().split(":", 1) if u == USER and p == PASS: ts = int(time.time()) self.send_response(200) self.send_header("Set-Cookie", f"{COOKIE_NAME}={ts}.{make_token(ts)}; Path=/; Max-Age={COOKIE_MAX_AGE}; HttpOnly; SameSite=Lax") self.end_headers(); return except: pass self.send_response(401) self.send_header("WWW-Authenticate", 'Basic realm="Sovereign Stack"') self.end_headers() if __name__ == "__main__": s = http.server.HTTPServer(("127.0.0.1", 9876), Handler) print("Auth gate on 127.0.0.1:9876"); s.serve_forever() AUTHGATE chmod +x "$PROJECT_DIR/auth-gate.py" } # --- Nginx (Tailscale-only) --- install_nginx() { step "Configuring nginx (Tailscale-only: $TSIP)..." cat > /etc/nginx/sites-available/paperclip < "$SECRETS_FILE" </dev/null || pnpm install cd - > /dev/null install_auth_gate install_nginx step "Paperclip installed" } # --- systemd services --- install_systemd() { banner "systemd Services" # Auth gate cat > /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 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 sleep 1 } start_services() { banner "Starting" load_or_create_secrets kill_zombies systemctl restart sovereign-auth-gate systemctl restart nginx systemctl restart sovereign-paperclip step "Waiting for Paperclip..." for i in $(seq 1 30); do 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 "" info "══════════════════════════════════════" info " Paperclip: http://$TSIP" info " Hermes: ssh root@$TSIP → hermes" info " Auth: $AUTH_USER / ********" info "══════════════════════════════════════" } stop_services() { banner "Stopping" 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" } show_status() { banner "Status" 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} $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" else echo -e " ${RED}○${NC} :$port" fi done } # ============================================================================= # 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) full_install ;; start) start_services ;; stop) stop_services ;; restart) stop_services; sleep 2; start_services ;; status) show_status ;; 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