v7: Paperclip only — stripped OpenFang and Obsidian vault
This commit is contained in:
317
setup_timmy.sh
317
setup_timmy.sh
@@ -1,68 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# =============================================================================
|
||||
# Sovereign Agent Stack — VPS Deployment Script v6
|
||||
# Sovereign Agent Stack — VPS Deployment Script v7
|
||||
#
|
||||
# Bootstraps Paperclip + OpenFang + Obsidian Vault on a VPS with:
|
||||
# Paperclip only. No fluff.
|
||||
# - Paperclip in local_trusted mode (127.0.0.1:3100)
|
||||
# - Nginx reverse proxy on port 80 with cookie-based auth gate
|
||||
# - One login prompt, 7-day session cookie — no repeated auth popups
|
||||
# - Nginx reverse proxy on port 80
|
||||
# - Cookie-based auth gate — login once, 7-day session
|
||||
# - PostgreSQL backend
|
||||
#
|
||||
# Usage:
|
||||
# 1. curl -O https://raw.githubusercontent.com/AlexanderWhitestone/Timmy-time-dashboard/main/setup_timmy.sh
|
||||
# 2. chmod +x setup_timmy.sh
|
||||
# 3. ./setup_timmy.sh install
|
||||
# 4. ./setup_timmy.sh start
|
||||
# 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
|
||||
#
|
||||
# Dashboard: http://YOUR_DOMAIN (behind auth gate)
|
||||
# Dashboard: http://YOUR_DOMAIN (behind auth)
|
||||
# =============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# --- Configuration ---
|
||||
PROJECT_DIR="${PROJECT_DIR:-$HOME/sovereign-stack}"
|
||||
VAULT_DIR="${VAULT_DIR:-$HOME/TimmyVault}"
|
||||
PAPERCLIP_DIR="$PROJECT_DIR/paperclip"
|
||||
AGENTS_DIR="$PROJECT_DIR/agents"
|
||||
LOG_DIR="$PROJECT_DIR/logs"
|
||||
PID_DIR="$PROJECT_DIR/pids"
|
||||
|
||||
# Domain / IP — set DOMAIN env var or edit here
|
||||
DOMAIN="${DOMAIN:-$(curl -s ifconfig.me)}"
|
||||
|
||||
# Database
|
||||
DB_USER="paperclip"
|
||||
DB_PASS="paperclip"
|
||||
DB_NAME="paperclip"
|
||||
DATABASE_URL="postgresql://$DB_USER:$DB_PASS@localhost:5432/$DB_NAME"
|
||||
|
||||
# Auth gate credentials — change these!
|
||||
AUTH_USER="${AUTH_USER:-Rockachopa}"
|
||||
AUTH_PASS="${AUTH_PASS:-Iamrockachopathegend}"
|
||||
|
||||
# Paperclip secrets (generated once, persisted in secrets file)
|
||||
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'
|
||||
CYAN='\033[0;36m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; BOLD='\033[1m'; NC='\033[0m'
|
||||
|
||||
# --- Helper Functions ---
|
||||
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}"; }
|
||||
|
||||
# --- Secrets Management ---
|
||||
# --- Secrets ---
|
||||
load_or_create_secrets() {
|
||||
if [ -f "$SECRETS_FILE" ]; then
|
||||
source "$SECRETS_FILE"
|
||||
step "Loaded existing secrets"
|
||||
step "Loaded secrets"
|
||||
else
|
||||
BETTER_AUTH_SECRET="sovereign-$(openssl rand -hex 16)"
|
||||
PAPERCLIP_AGENT_JWT_SECRET="agent-$(openssl rand -hex 16)"
|
||||
@@ -71,24 +60,18 @@ BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET"
|
||||
PAPERCLIP_AGENT_JWT_SECRET="$PAPERCLIP_AGENT_JWT_SECRET"
|
||||
SECRETS
|
||||
chmod 600 "$SECRETS_FILE"
|
||||
step "Generated and saved new secrets"
|
||||
step "Generated secrets"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Preflight ---
|
||||
check_preflight() {
|
||||
banner "Preflight Checks"
|
||||
|
||||
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
step "OS: Linux detected"
|
||||
else
|
||||
warn "OS: $OSTYPE — this script targets Ubuntu/Debian"
|
||||
fi
|
||||
banner "Preflight"
|
||||
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
step "Installing system dependencies..."
|
||||
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 apache2-utils > /dev/null 2>&1
|
||||
sudo apt-get install -y curl git postgresql postgresql-contrib build-essential nginx > /dev/null 2>&1
|
||||
fi
|
||||
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
@@ -107,11 +90,9 @@ check_preflight() {
|
||||
|
||||
# --- Database ---
|
||||
setup_database() {
|
||||
banner "Database Setup"
|
||||
step "Ensuring PostgreSQL is running..."
|
||||
banner "Database"
|
||||
sudo systemctl start postgresql || sudo service postgresql start || true
|
||||
|
||||
step "Configuring database..."
|
||||
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;"
|
||||
|
||||
@@ -121,56 +102,13 @@ setup_database() {
|
||||
step "Database ready"
|
||||
}
|
||||
|
||||
# --- Install Stack ---
|
||||
install_stack() {
|
||||
banner "Installing Sovereign Stack"
|
||||
mkdir -p "$PROJECT_DIR" "$AGENTS_DIR" "$LOG_DIR" "$PID_DIR"
|
||||
|
||||
# OpenFang
|
||||
if ! command -v openfang >/dev/null 2>&1; then
|
||||
step "Installing OpenFang..."
|
||||
curl -fsSL https://openfang.sh/install | sh
|
||||
fi
|
||||
step "OpenFang ready"
|
||||
|
||||
# Paperclip
|
||||
if [ ! -d "$PAPERCLIP_DIR" ]; then
|
||||
step "Cloning Paperclip..."
|
||||
git clone --depth 1 https://github.com/paperclipai/paperclip.git "$PAPERCLIP_DIR"
|
||||
fi
|
||||
cd "$PAPERCLIP_DIR"
|
||||
step "Installing Paperclip dependencies..."
|
||||
pnpm install --frozen-lockfile 2>/dev/null || pnpm install
|
||||
cd - > /dev/null
|
||||
step "Paperclip ready"
|
||||
|
||||
# Obsidian Vault
|
||||
mkdir -p "$VAULT_DIR"
|
||||
if [ ! -f "$VAULT_DIR/Genesis.md" ]; then
|
||||
cat > "$VAULT_DIR/Genesis.md" <<MD
|
||||
# Genesis Note
|
||||
Created on $(date)
|
||||
|
||||
Timmy's sovereign knowledge vault.
|
||||
MD
|
||||
fi
|
||||
step "Obsidian Vault ready: $VAULT_DIR"
|
||||
|
||||
# Install auth gate and nginx config
|
||||
install_auth_gate
|
||||
install_nginx
|
||||
|
||||
banner "Installation Complete"
|
||||
info "Run: ./setup_timmy.sh start"
|
||||
}
|
||||
|
||||
# --- Auth Gate (cookie-based, login once) ---
|
||||
# --- Auth Gate ---
|
||||
install_auth_gate() {
|
||||
step "Installing auth gate..."
|
||||
|
||||
cat > "$PROJECT_DIR/auth-gate.py" <<'AUTHGATE'
|
||||
#!/usr/bin/env python3
|
||||
"""Cookie-based auth gate for nginx auth_request. Login once, session lasts 7 days."""
|
||||
"""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")
|
||||
@@ -180,75 +118,55 @@ COOKIE_NAME = "sovereign_gate"
|
||||
COOKIE_MAX_AGE = 86400 * 7
|
||||
|
||||
def make_token(ts):
|
||||
msg = f"{USER}:{ts}".encode()
|
||||
return hmac.new(SECRET.encode(), msg, hashlib.sha256).hexdigest()[:32]
|
||||
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
|
||||
if len(parts) != 2: return False
|
||||
ts, sig = int(parts[0]), parts[1]
|
||||
if time.time() - ts > COOKIE_MAX_AGE:
|
||||
return False
|
||||
if time.time() - ts > COOKIE_MAX_AGE: return False
|
||||
return sig == make_token(ts)
|
||||
except:
|
||||
return False
|
||||
except: return False
|
||||
|
||||
class Handler(http.server.BaseHTTPRequestHandler):
|
||||
def log_message(self, *a): pass
|
||||
|
||||
def do_GET(self):
|
||||
cookies = self.headers.get("Cookie", "")
|
||||
for c in cookies.split(";"):
|
||||
for c in self.headers.get("Cookie", "").split(";"):
|
||||
c = c.strip()
|
||||
if c.startswith(f"{COOKIE_NAME}="):
|
||||
token = c[len(COOKIE_NAME)+1:]
|
||||
if verify_token(token):
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
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:
|
||||
decoded = base64.b64decode(auth[6:]).decode()
|
||||
u, p = decoded.split(":", 1)
|
||||
u, p = base64.b64decode(auth[6:]).decode().split(":", 1)
|
||||
if u == USER and p == PASS:
|
||||
ts = int(time.time())
|
||||
token = f"{ts}.{make_token(ts)}"
|
||||
self.send_response(200)
|
||||
self.send_header("Set-Cookie",
|
||||
f"{COOKIE_NAME}={token}; Path=/; Max-Age={COOKIE_MAX_AGE}; HttpOnly; SameSite=Lax")
|
||||
self.end_headers()
|
||||
return
|
||||
except:
|
||||
pass
|
||||
|
||||
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 listening on 127.0.0.1:9876")
|
||||
s.serve_forever()
|
||||
print("Auth gate on 127.0.0.1:9876"); s.serve_forever()
|
||||
AUTHGATE
|
||||
|
||||
chmod +x "$PROJECT_DIR/auth-gate.py"
|
||||
step "Auth gate installed"
|
||||
}
|
||||
|
||||
# --- Nginx Reverse Proxy ---
|
||||
# --- Nginx ---
|
||||
install_nginx() {
|
||||
step "Configuring nginx reverse proxy..."
|
||||
step "Configuring nginx..."
|
||||
|
||||
cat > /etc/nginx/sites-available/paperclip <<NGINX
|
||||
server {
|
||||
listen 80;
|
||||
server_name $DOMAIN;
|
||||
|
||||
# Cookie-based auth gate — login once, cookie lasts 7 days
|
||||
location = /_auth {
|
||||
internal;
|
||||
proxy_pass http://127.0.0.1:9876;
|
||||
@@ -268,7 +186,6 @@ server {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade \$http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
# Pass localhost as Host to bypass Vite's allowedHosts check
|
||||
proxy_set_header Host localhost;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
@@ -289,54 +206,63 @@ NGINX
|
||||
|
||||
ln -sf /etc/nginx/sites-available/paperclip /etc/nginx/sites-enabled/paperclip
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
nginx -t || fail "Nginx config test failed"
|
||||
step "Nginx configured"
|
||||
nginx -t || fail "Nginx config failed"
|
||||
}
|
||||
|
||||
# --- Process Cleanup ---
|
||||
# --- Install ---
|
||||
install_stack() {
|
||||
banner "Installing Paperclip"
|
||||
mkdir -p "$PROJECT_DIR" "$LOG_DIR" "$PID_DIR"
|
||||
|
||||
if [ ! -d "$PAPERCLIP_DIR" ]; then
|
||||
step "Cloning Paperclip..."
|
||||
git clone --depth 1 https://github.com/paperclipai/paperclip.git "$PAPERCLIP_DIR"
|
||||
fi
|
||||
cd "$PAPERCLIP_DIR"
|
||||
step "Installing dependencies..."
|
||||
pnpm install --frozen-lockfile 2>/dev/null || pnpm install
|
||||
cd - > /dev/null
|
||||
|
||||
install_auth_gate
|
||||
install_nginx
|
||||
|
||||
banner "Installed"
|
||||
info "Run: ./setup_timmy.sh start"
|
||||
}
|
||||
|
||||
# --- Cleanup ---
|
||||
kill_zombies() {
|
||||
step "Cleaning up stale processes..."
|
||||
# Kill any existing Paperclip/node processes on ports 3100-3110
|
||||
step "Cleaning stale processes..."
|
||||
for port in $(seq 3100 3110); do
|
||||
pid=$(lsof -ti :$port 2>/dev/null || true)
|
||||
if [ -n "$pid" ]; then
|
||||
kill -9 $pid 2>/dev/null || true
|
||||
step "Killed process on port $port"
|
||||
fi
|
||||
[ -n "$pid" ] && kill -9 $pid 2>/dev/null && step "Killed port $port" || true
|
||||
done
|
||||
# Kill any stale auth gate
|
||||
pid=$(lsof -ti :9876 2>/dev/null || true)
|
||||
if [ -n "$pid" ]; then
|
||||
kill -9 $pid 2>/dev/null || true
|
||||
fi
|
||||
[ -n "$pid" ] && kill -9 $pid 2>/dev/null || true
|
||||
sleep 1
|
||||
}
|
||||
|
||||
# --- Start Services ---
|
||||
# --- Start ---
|
||||
start_services() {
|
||||
banner "Starting Services"
|
||||
|
||||
banner "Starting"
|
||||
load_or_create_secrets
|
||||
kill_zombies
|
||||
|
||||
# Stop Docker Caddy if it's hogging port 80
|
||||
# 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 conflict)..."
|
||||
step "Stopping Docker Caddy (port 80)..."
|
||||
docker stop timmy-caddy > /dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# 1. Auth Gate
|
||||
step "Starting auth gate..."
|
||||
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"
|
||||
|
||||
# 2. Nginx
|
||||
step "Starting nginx..."
|
||||
step "Nginx..."
|
||||
systemctl restart nginx
|
||||
|
||||
# 3. Paperclip — local_trusted mode, bound to 127.0.0.1
|
||||
step "Starting Paperclip (local_trusted → 127.0.0.1:3100)..."
|
||||
step "Paperclip (local_trusted → 127.0.0.1:3100)..."
|
||||
cd "$PAPERCLIP_DIR"
|
||||
HOST=127.0.0.1 \
|
||||
DATABASE_URL="$DATABASE_URL" \
|
||||
@@ -348,115 +274,64 @@ start_services() {
|
||||
echo $! > "$PID_DIR/paperclip.pid"
|
||||
cd - > /dev/null
|
||||
|
||||
# 4. OpenFang
|
||||
if command -v openfang >/dev/null 2>&1; then
|
||||
step "Starting OpenFang..."
|
||||
nohup openfang start > "$LOG_DIR/openfang.log" 2>&1 &
|
||||
echo $! > "$PID_DIR/openfang.pid"
|
||||
fi
|
||||
|
||||
# Wait for Paperclip
|
||||
step "Waiting for Paperclip to initialize..."
|
||||
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 "║ Sovereign Stack is LIVE ║"
|
||||
info "║ ║"
|
||||
info "║ Dashboard: http://$DOMAIN"
|
||||
info "║ Auth: $AUTH_USER / ******** ║"
|
||||
info "║ Mode: local_trusted + nginx proxy ║"
|
||||
info "╚═══════════════════════════════════════════════╝"
|
||||
info "══════════════════════════════════════"
|
||||
info " Dashboard: http://$DOMAIN"
|
||||
info " Auth: $AUTH_USER / ********"
|
||||
info "══════════════════════════════════════"
|
||||
return
|
||||
fi
|
||||
printf "."
|
||||
sleep 2
|
||||
fi
|
||||
printf "."; sleep 2
|
||||
done
|
||||
echo ""
|
||||
warn "Startup taking longer than expected. Check: tail -f $LOG_DIR/paperclip.log"
|
||||
echo ""; warn "Slow start. Check: tail -f $LOG_DIR/paperclip.log"
|
||||
}
|
||||
|
||||
# --- Stop Services ---
|
||||
# --- Stop ---
|
||||
stop_services() {
|
||||
banner "Stopping Services"
|
||||
for service in paperclip openfang auth-gate; do
|
||||
banner "Stopping"
|
||||
for service in paperclip auth-gate; do
|
||||
pid_file="$PID_DIR/$service.pid"
|
||||
if [ -f "$pid_file" ]; then
|
||||
pid=$(cat "$pid_file")
|
||||
if ps -p "$pid" > /dev/null 2>&1; then
|
||||
step "Stopping $service (PID: $pid)..."
|
||||
kill "$pid" 2>/dev/null || true
|
||||
# Also kill child processes
|
||||
pkill -P "$pid" 2>/dev/null || true
|
||||
fi
|
||||
ps -p "$pid" > /dev/null 2>&1 && kill "$pid" 2>/dev/null && step "Stopped $service" || true
|
||||
rm -f "$pid_file"
|
||||
fi
|
||||
done
|
||||
step "Services stopped"
|
||||
}
|
||||
|
||||
# --- Status ---
|
||||
show_status() {
|
||||
banner "Stack Status"
|
||||
for s in paperclip openfang auth-gate; do
|
||||
pid_file="$PID_DIR/$s.pid"
|
||||
if [ -f "$pid_file" ] && ps -p $(cat "$pid_file") > /dev/null 2>&1; then
|
||||
echo -e " ${GREEN}●${NC} $s (PID $(cat "$pid_file"))"
|
||||
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"
|
||||
else
|
||||
echo -e " ${RED}○${NC} $s"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
# Port check
|
||||
for port in 80 3100 9876; do
|
||||
pid=$(lsof -ti :$port 2>/dev/null || true)
|
||||
if [ -n "$pid" ]; then
|
||||
echo -e " ${GREEN}●${NC} Port $port in use"
|
||||
if lsof -ti :$port > /dev/null 2>&1; then
|
||||
echo -e " ${GREEN}●${NC} :$port"
|
||||
else
|
||||
echo -e " ${RED}○${NC} Port $port free"
|
||||
echo -e " ${RED}○${NC} :$port"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
if systemctl is-active --quiet nginx; then
|
||||
echo -e " ${GREEN}●${NC} nginx active"
|
||||
else
|
||||
echo -e " ${RED}○${NC} nginx inactive"
|
||||
fi
|
||||
systemctl is-active --quiet nginx && echo -e " ${GREEN}●${NC} nginx" || echo -e " ${RED}○${NC} nginx"
|
||||
}
|
||||
|
||||
# --- Restart ---
|
||||
restart_services() {
|
||||
stop_services
|
||||
sleep 2
|
||||
start_services
|
||||
}
|
||||
|
||||
# --- Main CLI ---
|
||||
# --- CLI ---
|
||||
case "${1:-}" in
|
||||
install)
|
||||
check_preflight
|
||||
setup_database
|
||||
install_stack
|
||||
;;
|
||||
start)
|
||||
start_services
|
||||
;;
|
||||
stop)
|
||||
stop_services
|
||||
;;
|
||||
restart)
|
||||
restart_services
|
||||
;;
|
||||
status)
|
||||
show_status
|
||||
;;
|
||||
logs)
|
||||
tail -f "$LOG_DIR"/*.log
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {install|start|stop|restart|status|logs}"
|
||||
exit 1
|
||||
;;
|
||||
install) check_preflight; setup_database; install_stack ;;
|
||||
start) start_services ;;
|
||||
stop) stop_services ;;
|
||||
restart) stop_services; sleep 2; start_services ;;
|
||||
status) show_status ;;
|
||||
logs) tail -f "$LOG_DIR"/*.log ;;
|
||||
*) echo "Usage: $0 {install|start|stop|restart|status|logs}"; exit 1 ;;
|
||||
esac
|
||||
|
||||
Reference in New Issue
Block a user