#!/usr/bin/env bash set -Eeuo pipefail readonly CONFIG_DIR="/etc/ochenstarik-server" readonly AGENT_CONFIG="${CONFIG_DIR}/ai-agents.conf" readonly STEP3_ENV="/root/setup-data/env.txt" readonly -a AGENT_IDS=(hermes openclaw openhands opencode aider autogpt pi) readonly -a AGENT_NAMES=( "Hermes Agent" "OpenClaw" "OpenHands" "OpenCode" "Aider" "AutoGPT" "Pi Coding Agent" ) readonly -a AGENT_DESCRIPTIONS=( "Personal self-improving agent with memory, skills, schedules, and messaging gateways" "Personal assistant with persistent memory and Telegram, WhatsApp, Discord, Slack, and other channels" "Software-development agent with terminal, headless, web, and IDE modes" "Terminal coding agent with multiple model providers and parallel sessions" "Git-oriented coding agent for editing and reviewing an existing repository" "Docker-based platform for building, deploying, and running autonomous agents; may request sudo" "Minimal and extensible terminal coding agent with skills, extensions, and multiple providers" ) readonly -a AGENT_INSTALL_URLS=( "https://hermes-agent.nousresearch.com/install.sh" "https://openclaw.ai/install.sh" "https://install.openhands.dev/install.sh" "https://opencode.ai/install" "https://aider.chat/install.sh" "https://setup.agpt.co/install.sh" "https://pi.dev/install.sh" ) readonly -a AGENT_COMMANDS=(hermes openclaw openhands opencode aider docker pi) readonly -a AGENT_SETUP_HINTS=( "hermes setup" "openclaw onboard --install-daemon" "openhands" "opencode" "aider" "Open the local AutoGPT address printed by the official installer" "pi, then use /login to connect a provider" ) TEMP_INSTALLER="" log() { printf '[+] %s\n' "$*"; } warn() { printf '[!] %s\n' "$*" >&2; } die() { printf '[x] %s\n' "$*" >&2; exit 1; } cleanup() { if [[ -n "$TEMP_INSTALLER" && "$TEMP_INSTALLER" == /tmp/ochenstarik-ai-installer.* \ && -f "$TEMP_INSTALLER" && ! -L "$TEMP_INSTALLER" ]]; then rm -f -- "$TEMP_INSTALLER" fi } trap cleanup EXIT require_command() { command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1" } default_target_user() { local username="" if [[ -f "$STEP3_ENV" && ! -L "$STEP3_ENV" ]]; then username="$(awk -F= '$1 == "NEW_USERNAME" && !found { sub(/\r$/, "", $2); print $2; found=1 }' "$STEP3_ENV")" if [[ "$username" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] && id "$username" >/dev/null 2>&1; then printf '%s' "$username" return 0 fi fi getent passwd | awk -F: '$3 >= 1000 && $3 != 65534 && $1 != "nobody" && !found { print $1; found=1 }' } select_target_user() { local default_user username uid home owner default_user="$(default_target_user)" printf '\nAI agents must be installed for a regular user, never for root.\n' while :; do if [[ -n "$default_user" ]]; then read -rp "Installation user [${default_user}]: " username || die "Input was interrupted" username="${username:-$default_user}" else read -rp 'Installation user: ' username || die "Input was interrupted" fi [[ "$username" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || { warn "Invalid username"; continue; } [[ "$username" != root ]] || { warn "Root cannot be used for AI agents"; continue; } id "$username" >/dev/null 2>&1 || { warn "User does not exist: $username"; continue; } uid="$(id -u "$username")" ((uid >= 1000 && uid != 65534)) || { warn "System accounts are not allowed"; continue; } home="$(getent passwd "$username" | cut -d: -f6)" [[ -n "$home" && "$home" == /* && -d "$home" && ! -L "$home" ]] \ || { warn "A safe home directory was not found for $username"; continue; } owner="$(stat -c %U "$home")" [[ "$owner" == "$username" ]] || { warn "$home must be owned by $username"; continue; } TARGET_USER="$username" TARGET_HOME="$home" TARGET_GROUP="$(id -gn "$username")" return 0 done } select_agents() { local input token index local -A seen=() SELECTED_INDEXES=() printf '\nSelect one or more AI agents to install:\n' for index in "${!AGENT_IDS[@]}"; do printf ' %d) %s\n %s\n Official installer: %s\n' \ "$((index + 1))" "${AGENT_NAMES[index]}" "${AGENT_DESCRIPTIONS[index]}" \ "${AGENT_INSTALL_URLS[index]}" done printf ' 0) Do not install AI agents\n' printf '\nEnter numbers separated by spaces, for example: 1 2 4.\n' while :; do read -rp 'Selection [0]: ' input || die "Input was interrupted" input="${input//,/ }" input="${input:-0}" if [[ "$input" =~ ^[[:space:]]*0[[:space:]]*$ ]]; then return 0 fi SELECTED_INDEXES=() seen=() for token in $input; do if [[ ! "$token" =~ ^[1-7]$ ]]; then warn "Enter agent numbers from 1 to 7, or 0 to skip" SELECTED_INDEXES=() break fi index="$((token - 1))" if [[ -z "${seen[$index]:-}" ]]; then SELECTED_INDEXES+=("$index") seen["$index"]=1 fi done ((${#SELECTED_INDEXES[@]} > 0)) && return 0 done } install_agent() { local index="$1" name url command_name path_value name="${AGENT_NAMES[index]}" url="${AGENT_INSTALL_URLS[index]}" command_name="${AGENT_COMMANDS[index]}" path_value="${TARGET_HOME}/.local/bin:${TARGET_HOME}/.opencode/bin:${TARGET_HOME}/.hermes/bin:${TARGET_HOME}/.local/share/pnpm:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" log "Downloading the official ${name} installer" TEMP_INSTALLER="$(mktemp /tmp/ochenstarik-ai-installer.XXXXXX)" chmod 600 "$TEMP_INSTALLER" curl -fL --retry 5 --retry-delay 5 --connect-timeout 30 \ --proto '=https' --tlsv1.2 "$url" -o "$TEMP_INSTALLER" [[ -s "$TEMP_INSTALLER" && ! -L "$TEMP_INSTALLER" ]] \ || die "The downloaded ${name} installer is empty or unsafe" bash -n "$TEMP_INSTALLER" || die "The official ${name} installer failed Bash syntax validation" # Keep the validated file root-owned until execution so the target account # cannot replace it between validation and launch. chown root:root "$TEMP_INSTALLER" chmod 555 "$TEMP_INSTALLER" log "Installing ${name} for ${TARGET_USER}" runuser -u "$TARGET_USER" -- env \ HOME="$TARGET_HOME" USER="$TARGET_USER" LOGNAME="$TARGET_USER" SHELL=/bin/bash PATH="$path_value" \ bash -c 'cd "$HOME" && exec bash "$1"' ochenstarik-agent-installer "$TEMP_INSTALLER" rm -f -- "$TEMP_INSTALLER" TEMP_INSTALLER="" if runuser -u "$TARGET_USER" -- env HOME="$TARGET_HOME" PATH="$path_value" \ bash -lc "command -v '$command_name' >/dev/null 2>&1"; then log "${name} command is available: ${command_name}" else warn "${name} installer completed, but ${command_name} is not visible in a new login shell yet" warn "Log in again or reload the user's shell configuration before running it" fi } write_state() { local ids="" index install -d -m 700 -o root -g root "$CONFIG_DIR" [[ ! -L "$AGENT_CONFIG" ]] || die "$AGENT_CONFIG must not be a symbolic link" for index in "${SELECTED_INDEXES[@]}"; do ids+="${ids:+ }${AGENT_IDS[index]}" done umask 077 printf 'TARGET_USER=%s\nAGENTS=%s\n' "$TARGET_USER" "$ids" > "$AGENT_CONFIG" chown root:root "$AGENT_CONFIG" chmod 600 "$AGENT_CONFIG" } [[ "$EUID" -eq 0 ]] || die "Run this script as root" require_command awk require_command apt-get require_command bash require_command getent require_command runuser require_command stat ((${#AGENT_IDS[@]} == 7 && ${#AGENT_NAMES[@]} == 7 \ && ${#AGENT_DESCRIPTIONS[@]} == 7 && ${#AGENT_INSTALL_URLS[@]} == 7 \ && ${#AGENT_COMMANDS[@]} == 7 && ${#AGENT_SETUP_HINTS[@]} == 7)) \ || die "Invalid AI agent catalog" select_target_user select_agents if ((${#SELECTED_INDEXES[@]} == 0)); then log "AI agent installation was skipped" exit 0 fi export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install -y curl ca-certificates git require_command curl printf '\nThe selected agents can execute commands, access files, and use network services.\n' printf 'Review permissions, tools, plugins, mounted directories, and API-key access during onboarding.\n' read -rp "Type INSTALL to continue for user ${TARGET_USER}: " confirmation || die "Input was interrupted" [[ "$confirmation" == INSTALL ]] || die "AI agent installation was cancelled" for selected_index in "${SELECTED_INDEXES[@]}"; do install_agent "$selected_index" done write_state printf '\nInstalled AI agents for %s:\n' "$TARGET_USER" for selected_index in "${SELECTED_INDEXES[@]}"; do printf ' - %s\n Next step: %s\n' \ "${AGENT_NAMES[selected_index]}" "${AGENT_SETUP_HINTS[selected_index]}" done printf '\nRun the setup commands while logged in as %s, not as root.\n' "$TARGET_USER" printf 'API keys are managed by each agent and were not requested or stored by this script.\n'