#!/bin/bash # VFAT AI Agent Installation Script # Interactive installer with multiple modes # # Usage: # curl -fsSL https://staging.ai.vf.at/install.sh | TOKEN=xxx bash # # Environment variables: # TOKEN - Required: Agent authentication token # INSTALL_MODE - Optional: user or isolated (skips interactive prompt) # CONTROL_PLANE_URL - Optional: WebSocket URL (auto-set by server) # # Note: URLs below are replaced dynamically when served via /install.sh endpoint # set -e # If we're being piped (stdin is not a tty and $0 is bash), save to temp file # This allows re-execution with sudo for isolated mode if [ ! -t 0 ] && echo "$0" | grep -q "bash"; then SCRIPT_FILE=$(mktemp /tmp/vfat-ai-install.XXXXXX) cat > "$SCRIPT_FILE" chmod +x "$SCRIPT_FILE" export VFAT_AI_SCRIPT_FILE="$SCRIPT_FILE" exec bash "$SCRIPT_FILE" "$@" fi # Configuration from environment TOKEN="${TOKEN:-}" CONTROL_PLANE_URL="${CONTROL_PLANE_URL:-wss://staging.ai.vf.at/agent/connect}" INSTALL_MODE="${INSTALL_MODE:-}" UNINSTALL="${UNINSTALL:-false}" UNINSTALL_MODE="${UNINSTALL_MODE:-}" INSTALL_CLAUDE="${INSTALL_CLAUDE:-true}" INSTALL_CODEX="${INSTALL_CODEX:-true}" INSTALL_GEMINI="${INSTALL_GEMINI:-true}" INSTALL_NVM="${INSTALL_NVM:-true}" CLAUDE_INSTALL_CMD="${CLAUDE_INSTALL_CMD:-npm install -g @anthropic-ai/claude-code}" CODEX_INSTALL_CMD="${CODEX_INSTALL_CMD:-npm install -g @openai/codex}" GEMINI_INSTALL_CMD="${GEMINI_INSTALL_CMD:-npm install -g @google/gemini-cli}" NODE_VERSION="${NODE_VERSION:-lts/*}" CONTROL_PLANE_HOST="${CONTROL_PLANE_URL#ws://}" CONTROL_PLANE_HOST="${CONTROL_PLANE_HOST#wss://}" CONTROL_PLANE_HOST="${CONTROL_PLANE_HOST%%/agent/connect*}" SERVICE_NAME="vfat-ai-agent" if echo "$CONTROL_PLANE_HOST" | grep -qi "staging"; then SERVICE_NAME="vfat-ai-agent-staging" fi is_true() { case "$1" in true|TRUE|yes|YES|y|Y|1) return 0 ;; *) return 1 ;; esac } uninstall_user_mode() { echo "--- Uninstalling User Mode ---" local USER_SERVICE_DIR="$HOME/.config/systemd/user" local USER_ENV_FILE="$HOME/.config/vfat-ai/agent.env" local USER_VENV="$HOME/.local/vfat-ai" if command -v systemctl >/dev/null 2>&1; then systemctl --user stop vfat-ai-agent vfat-ai-agent-staging 2>/dev/null || true systemctl --user disable vfat-ai-agent vfat-ai-agent-staging 2>/dev/null || true fi rm -f "$USER_SERVICE_DIR/vfat-ai-agent.service" "$USER_SERVICE_DIR/vfat-ai-agent-staging.service" rm -rf "$USER_VENV" rm -f "$USER_ENV_FILE" if command -v systemctl >/dev/null 2>&1; then systemctl --user daemon-reload 2>/dev/null || true fi echo "User-mode service and venv removed." } uninstall_isolated_mode() { echo "--- Uninstalling Isolated Mode ---" if [ "$EUID" -ne 0 ]; then echo "Isolated mode uninstall requires root privileges." echo "Re-running with sudo..." exec sudo UNINSTALL="$UNINSTALL" UNINSTALL_MODE="isolated" CONTROL_PLANE_URL="$CONTROL_PLANE_URL" bash "$0" fi systemctl stop vfat-ai-agent vfat-ai-agent-staging 2>/dev/null || true systemctl disable vfat-ai-agent vfat-ai-agent-staging 2>/dev/null || true rm -f /etc/systemd/system/vfat-ai-agent.service /etc/systemd/system/vfat-ai-agent-staging.service systemctl daemon-reload systemctl reset-failed rm -rf /var/lib/vfat-ai/venv rm -f /etc/vfat-ai-agent.env echo "Isolated service and venv removed." } auto_uninstall() { if [ -n "$UNINSTALL_MODE" ]; then case "$UNINSTALL_MODE" in user) uninstall_user_mode ;; isolated) uninstall_isolated_mode ;; *) echo "Unknown UNINSTALL_MODE: $UNINSTALL_MODE" exit 1 ;; esac return fi if [ -f /etc/systemd/system/vfat-ai-agent.service ] || [ -f /etc/systemd/system/vfat-ai-agent-staging.service ]; then uninstall_isolated_mode return fi if [ -f "$HOME/.config/systemd/user/vfat-ai-agent.service" ] || [ -f "$HOME/.config/systemd/user/vfat-ai-agent-staging.service" ]; then uninstall_user_mode return fi if [ -d /var/lib/vfat-ai/venv ]; then uninstall_isolated_mode return fi if [ -d "$HOME/.local/vfat-ai" ]; then uninstall_user_mode return fi echo "No VFAT AI agent installation detected." } install_nvm_and_node() { echo "Installing nvm and Node.js (${NODE_VERSION})..." if ! command -v curl >/dev/null 2>&1; then echo "ERROR: curl is required to install nvm" return 1 fi curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash export NVM_DIR="$HOME/.nvm" # shellcheck source=/dev/null [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" if command -v nvm >/dev/null 2>&1; then nvm install "$NODE_VERSION" else echo "ERROR: nvm install failed" return 1 fi } load_nvm_if_present() { if [ -z "${NVM_DIR:-}" ]; then export NVM_DIR="$HOME/.nvm" fi # shellcheck source=/dev/null [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" if command -v nvm >/dev/null 2>&1; then nvm use --silent default >/dev/null 2>&1 || nvm use --silent --lts >/dev/null 2>&1 || true fi } ensure_npm() { # Ensure nvm-managed npm is on PATH when installed load_nvm_if_present if command -v npm >/dev/null 2>&1; then return 0 fi if is_true "$INSTALL_NVM"; then install_nvm_and_node else echo "npm not found. Set INSTALL_NVM=true to install Node.js automatically." return 1 fi } install_cli_if_missing() { local name="$1" local cmd="$2" if command -v "$name" >/dev/null 2>&1; then return 0 fi if [ -z "$cmd" ]; then echo "Missing $name CLI. Provide ${name^^}_INSTALL_CMD to auto-install." echo "Example: ${name^^}_INSTALL_CMD='your install command'" return 1 fi echo "Installing $name CLI..." bash -lc "$cmd" } install_optional_clis() { load_nvm_if_present if is_true "$INSTALL_CLAUDE"; then if command -v claude >/dev/null 2>&1; then : else if ensure_npm; then install_cli_if_missing "claude" "$CLAUDE_INSTALL_CMD" fi fi fi if is_true "$INSTALL_CODEX"; then if command -v codex >/dev/null 2>&1; then : else if ensure_npm; then install_cli_if_missing "codex" "$CODEX_INSTALL_CMD" fi fi fi if is_true "$INSTALL_GEMINI"; then if command -v gemini >/dev/null 2>&1; then return 0 fi if ensure_npm; then install_cli_if_missing "gemini" "$GEMINI_INSTALL_CMD" fi fi } # Clean up temp file on exit if we created one if [ -n "$VFAT_AI_SCRIPT_FILE" ]; then trap "rm -f '$VFAT_AI_SCRIPT_FILE'" EXIT fi if is_true "$UNINSTALL"; then auto_uninstall exit 0 fi if [ -z "$TOKEN" ]; then echo "TOKEN is required. Get your token from https://staging.ai.vf.at" echo " curl -fsSL https://staging.ai.vf.at/install.sh | TOKEN=your-token bash" exit 1 fi # Derive package URL from control plane URL HTTPS_URL="${CONTROL_PLANE_URL/wss:/https:}" HTTPS_URL="${HTTPS_URL/ws:/http:}" PACKAGE_URL="${HTTPS_URL%/agent/connect}/package/vfat-ai.tar.gz" echo "=== VFAT AI Agent Installation ===" echo # Interactive mode selection (unless pre-selected) if [ -z "$INSTALL_MODE" ]; then echo "Choose installation mode:" echo echo " 1) User mode (recommended for personal dev machines)" echo " - Runs as your current user ($USER)" echo " - Uses ~/.local/vfat-ai/ for installation" echo " - Start manually or add to shell profile" echo echo " 2) Isolated mode (recommended for shared/production servers)" echo " - Creates dedicated 'vfat-ai' system user" echo " - Runs as systemd service (auto-start on boot)" echo " - More secure isolation" echo # Check if we have a tty for interactive input # When piped (curl | bash), stdin is the pipe, so we read from /dev/tty if [ -t 0 ]; then read -p "Enter choice [1/2]: " choice elif [ -e /dev/tty ]; then read -p "Enter choice [1/2]: " choice < /dev/tty else echo "No interactive terminal detected. Defaulting to user mode." echo "Use INSTALL_MODE=isolated for isolated mode." choice="1" fi case "$choice" in 1) INSTALL_MODE="user" ;; 2) INSTALL_MODE="isolated" ;; *) echo "Invalid choice. Defaulting to user mode." INSTALL_MODE="user" ;; esac fi echo echo "Selected mode: $INSTALL_MODE" echo # Function to install the package into a venv install_package() { local VENV_DIR="$1" local RUN_AS_USER="${2:-}" # Optional: run commands as this user echo "Creating virtual environment at $VENV_DIR..." if [ -n "$RUN_AS_USER" ]; then if ! sudo -u "$RUN_AS_USER" python3 -m venv "$VENV_DIR"; then echo "ERROR: Failed to create virtual environment" exit 1 fi else if ! python3 -m venv "$VENV_DIR"; then echo "ERROR: Failed to create virtual environment" exit 1 fi fi # Verify venv was created if [ ! -f "$VENV_DIR/bin/pip" ]; then echo "ERROR: Virtual environment creation failed - pip not found" echo "Make sure python3-venv is installed" exit 1 fi echo "Downloading package from $PACKAGE_URL..." if [ -n "$RUN_AS_USER" ]; then if ! sudo -u "$RUN_AS_USER" "$VENV_DIR/bin/pip" install --quiet "$PACKAGE_URL"; then echo "ERROR: Failed to install package" exit 1 fi else if ! "$VENV_DIR/bin/pip" install --quiet "$PACKAGE_URL"; then echo "ERROR: Failed to install package" exit 1 fi fi # Verify installation if [ ! -f "$VENV_DIR/bin/vfat-ai-agent" ]; then echo "ERROR: vfat-ai-agent not found after installation" echo "Check: $PACKAGE_URL" exit 1 fi echo "Installed: $("$VENV_DIR/bin/vfat-ai-agent" --version 2>/dev/null || echo 'vfat-ai-agent')" } # Function to install in user mode install_user_mode() { echo "--- Installing User Mode ---" VENV_DIR="$HOME/.local/vfat-ai" PROJECTS_DIR="$HOME/projects" ENV_FILE="$HOME/.config/vfat-ai/agent.env" AGENT_BIN="$VENV_DIR/bin/vfat-ai-agent" # Check for python3-venv if ! python3 -c "import ensurepip" 2>/dev/null; then echo "ERROR: python3-venv is required but not installed." echo echo "Install it with:" if command -v apt-get &>/dev/null; then echo " sudo apt install python3-venv" elif command -v dnf &>/dev/null; then echo " sudo dnf install python3-venv" elif command -v pacman &>/dev/null; then echo " sudo pacman -S python" else echo " Install python3-venv using your package manager" fi echo echo "Then re-run this script." exit 1 fi # Create directories echo "Setting up directories..." mkdir -p "$PROJECTS_DIR" mkdir -p "$(dirname "$ENV_FILE")" mkdir -p "$(dirname "$VENV_DIR")" install_optional_clis # Install package into venv install_package "$VENV_DIR" # Create environment file with token echo "Configuring agent token..." cat > "$ENV_FILE" << EOF # VFAT AI Agent Configuration (auto-generated) VFAT_AI_AGENT_TOKEN=$TOKEN VFAT_AI_CONTROL_PLANE_URL=$CONTROL_PLANE_URL EOF chmod 600 "$ENV_FILE" echo echo "User mode installed!" echo echo "Start the agent with:" echo " export \$(cat $ENV_FILE | xargs) && $AGENT_BIN" echo echo "Or add to your shell profile for persistence." } # Function to install in isolated mode install_isolated_mode() { echo "--- Installing Isolated Mode ---" # Check for root if [ "$EUID" -ne 0 ]; then echo "Isolated mode requires root privileges." echo "Re-running with sudo..." exec sudo TOKEN="$TOKEN" CONTROL_PLANE_URL="$CONTROL_PLANE_URL" INSTALL_MODE="isolated" bash "$0" fi AGENT_USER="vfat-ai" AGENT_HOME="/var/lib/vfat-ai" VENV_DIR="$AGENT_HOME/venv" PROJECTS_DIR="$AGENT_HOME/projects" ENV_FILE="/etc/vfat-ai-agent.env" AGENT_BIN="$VENV_DIR/bin/vfat-ai-agent" # Create system user if id "vfat-ai" &>/dev/null; then echo "User 'vfat-ai' already exists" else echo "Creating user 'vfat-ai'..." useradd --system \ --home-dir "$AGENT_HOME" \ --shell /bin/bash \ --create-home \ vfat-ai fi # Create directories echo "Setting up directories..." sudo -u vfat-ai mkdir -p "$PROJECTS_DIR" sudo -u vfat-ai mkdir -p "$AGENT_HOME/.ssh" chmod 700 "$AGENT_HOME/.ssh" echo "Installing optional CLIs..." sudo -u vfat-ai env \ INSTALL_CLAUDE="$INSTALL_CLAUDE" \ INSTALL_CODEX="$INSTALL_CODEX" \ INSTALL_GEMINI="$INSTALL_GEMINI" \ INSTALL_NVM="$INSTALL_NVM" \ CLAUDE_INSTALL_CMD="$CLAUDE_INSTALL_CMD" \ CODEX_INSTALL_CMD="$CODEX_INSTALL_CMD" \ GEMINI_INSTALL_CMD="$GEMINI_INSTALL_CMD" \ NODE_VERSION="$NODE_VERSION" \ bash -s <<'EOF' set -e is_true() { case "$1" in true|TRUE|yes|YES|y|Y|1) return 0 ;; *) return 1 ;; esac } install_nvm_and_node() { echo "Installing nvm and Node.js (${NODE_VERSION})..." if ! command -v curl >/dev/null 2>&1; then echo "ERROR: curl is required to install nvm" return 1 fi curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash export NVM_DIR="$HOME/.nvm" # shellcheck source=/dev/null [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" if command -v nvm >/dev/null 2>&1; then nvm install "$NODE_VERSION" else echo "ERROR: nvm install failed" return 1 fi } load_nvm_if_present() { if [ -z "${NVM_DIR:-}" ]; then export NVM_DIR="$HOME/.nvm" fi # shellcheck source=/dev/null [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" if command -v nvm >/dev/null 2>&1; then nvm use --silent default >/dev/null 2>&1 || nvm use --silent --lts >/dev/null 2>&1 || true fi } ensure_npm() { # Ensure nvm-managed npm is on PATH when installed load_nvm_if_present if command -v npm >/dev/null 2>&1; then return 0 fi if is_true "$INSTALL_NVM"; then install_nvm_and_node else echo "npm not found. Set INSTALL_NVM=true to install Node.js automatically." return 1 fi } install_cli_if_missing() { local name="$1" local cmd="$2" if command -v "$name" >/dev/null 2>&1; then return 0 fi if [ -z "$cmd" ]; then echo "Missing $name CLI. Provide ${name^^}_INSTALL_CMD to auto-install." echo "Example: ${name^^}_INSTALL_CMD='your install command'" return 1 fi echo "Installing $name CLI..." bash -lc "$cmd" } install_optional_clis() { load_nvm_if_present if is_true "$INSTALL_CLAUDE"; then if command -v claude >/dev/null 2>&1; then : else if ensure_npm; then install_cli_if_missing "claude" "$CLAUDE_INSTALL_CMD" fi fi fi if is_true "$INSTALL_CODEX"; then if command -v codex >/dev/null 2>&1; then : else if ensure_npm; then install_cli_if_missing "codex" "$CODEX_INSTALL_CMD" fi fi fi if is_true "$INSTALL_GEMINI"; then if command -v gemini >/dev/null 2>&1; then return 0 fi if ensure_npm; then install_cli_if_missing "gemini" "$GEMINI_INSTALL_CMD" fi fi } install_optional_clis EOF # Generate SSH key if [ ! -f "$AGENT_HOME/.ssh/id_ed25519" ]; then echo "Generating SSH key..." sudo -u vfat-ai ssh-keygen -t ed25519 \ -f "$AGENT_HOME/.ssh/id_ed25519" \ -N "" \ -C "vfat-ai-agent" fi # Configure git echo "Configuring git..." cd "$AGENT_HOME" sudo -u vfat-ai git config --global user.name "VFAT AI Agent" sudo -u vfat-ai git config --global user.email "agent@vfat-ai.local" # Install system dependencies (skip if already present) echo "Installing system dependencies..." if ! dpkg -s python3-venv python3-pip >/dev/null 2>&1; then apt-get update -qq || true # Ignore update errors (some repos may fail) if ! apt-get install -y -qq python3-venv python3-pip; then echo "ERROR: Failed to install python3-venv" echo "Try running: sudo apt install python3-venv python3-pip" exit 1 fi else echo "System dependencies already installed. Skipping apt." fi # Install package into venv install_package "$VENV_DIR" "vfat-ai" # Create environment file with token echo "Configuring agent token..." cat > "$ENV_FILE" << EOF # VFAT AI Agent Configuration (auto-generated) VFAT_AI_AGENT_TOKEN=$TOKEN VFAT_AI_CONTROL_PLANE_URL=$CONTROL_PLANE_URL VFAT_AI_SYSTEMD_SERVICE=$SERVICE_NAME EOF chmod 600 "$ENV_FILE" # Install systemd service echo "Installing systemd service..." cat > /etc/systemd/system/$SERVICE_NAME.service << EOF [Unit] Description=VFAT AI Agent ($CONTROL_PLANE_HOST) After=network.target [Service] Type=simple User=vfat-ai Group=vfat-ai WorkingDirectory=$AGENT_HOME EnvironmentFile=$ENV_FILE Environment=HOME=$AGENT_HOME ExecStart=$AGENT_BIN Restart=always RestartSec=10 [Install] WantedBy=multi-user.target EOF systemctl daemon-reload # Start the agent echo "Starting agent..." systemctl enable "$SERVICE_NAME" systemctl start "$SERVICE_NAME" echo echo "Isolated mode installed!" echo echo "Agent is running as systemd service ($SERVICE_NAME)." echo echo "Next step - Log in to Claude Code:" echo " sudo -u vfat-ai claude" } # Execute based on mode case "$INSTALL_MODE" in user) install_user_mode ;; isolated) install_isolated_mode ;; *) echo "Unknown mode: $INSTALL_MODE" exit 1 ;; esac echo echo "=== Installation Complete ===" echo echo "Return to the web UI to set up GitHub access."