#!/usr/bin/env bash # ============================================================================ # 🌌 AetherOS β€” Instalador # ============================================================================ # # Instala uma instΓ’ncia independente do AetherOS nesta mΓ‘quina. # # curl -fsSL https://install.pbpmdev.com | sudo bash # # A instΓ’ncia Γ© sua: senha prΓ³pria, banco prΓ³prio, chave de API prΓ³pria. Nada # aqui conecta na instΓ’ncia de outra pessoa. # # Acesso: a porta 8000 fica restrita Γ  Tailscale (a interface tailscale0). NΓ£o # Γ© exposta na internet β€” a senha Γ© a segunda barreira, nΓ£o a ΓΊnica. # set -euo pipefail VERMELHO='\033[0;31m'; VERDE='\033[0;32m'; AMARELO='\033[1;33m' AZUL='\033[0;34m'; CINZA='\033[0;90m'; NC='\033[0m' BASE_URL="${AETHEROS_BASE_URL:-https://install.pbpmdev.com}" DEST="${AETHEROS_DEST:-/root/aetheros}" SERVICE="aetheros" PORTA=8000 log() { echo -e "${AZUL}$*${NC}"; } ok() { echo -e "${VERDE} βœ“ $*${NC}"; } aviso() { echo -e "${AMARELO} ! $*${NC}"; } erro() { echo -e "${VERMELHO} βœ— $*${NC}" >&2; } morrer(){ erro "$*"; exit 1; } echo echo -e "${AZUL}══════════════════════════════════════════════════════════${NC}" echo -e "${AZUL} 🌌 AetherOS β€” Instalador${NC}" echo -e "${AZUL}══════════════════════════════════════════════════════════${NC}" echo # ── 0. PrΓ©-requisitos ────────────────────────────────────────────────────── [ "$(id -u)" -eq 0 ] || morrer "Rode como root (sudo)." . /etc/os-release 2>/dev/null || morrer "NΓ£o consegui identificar o sistema." case "${ID:-}${ID_LIKE:-}" in *debian*|*ubuntu*) : ;; *) morrer "SΓ³ suporto Ubuntu/Debian. Aqui Γ© '${PRETTY_NAME:-desconhecido}'." ;; esac ok "Sistema: ${PRETTY_NAME:-?}" # Uma instalaΓ§Γ£o por cima de outra apaga configuraΓ§Γ£o e banco sem avisar. SΓ³ # segue se o usuΓ‘rio disser explicitamente que Γ© isso que ele quer. if [ -e "$DEST" ] && [ -n "$(ls -A "$DEST" 2>/dev/null)" ]; then aviso "$DEST jΓ‘ existe e nΓ£o estΓ‘ vazio." if [ "${AETHEROS_FORCE:-}" != "1" ]; then echo " Pode ser uma instalaΓ§Γ£o anterior, com .env e banco." echo echo " Para reinstalar por cima (faz backup antes, e preserva" echo " banco e workspace se existirem), rode:" echo echo " curl -fsSL ${BASE_URL} -o /tmp/aether-install.sh" echo " sudo AETHEROS_FORCE=1 bash /tmp/aether-install.sh" echo exit 1 fi aviso "AETHEROS_FORCE=1 β€” reinstalando por cima." BKP="/root/aetheros-backup-$(date +%Y%m%d-%H%M%S).tar.gz" tar czf "$BKP" -C "$(dirname "$DEST")" "$(basename "$DEST")" 2>/dev/null || true ok "Backup completo: $BKP" # Remove o cΓ³digo antigo mas PRESERVA os dados do usuΓ‘rio. Sem isso, uma # instalaΓ§Γ£o anterior (inclusive de uma versΓ£o diferente, ou o app.py do # instalador antigo) deixa arquivo Γ³rfΓ£o solto em $DEST pra sempre β€” o # cp -a mais abaixo sΓ³ sobrepΓ΅e o que colide de nome. PRESERVAR="$(mktemp -d)" for d in backend/data workspace chroma_data; do if [ -e "$DEST/$d" ]; then mkdir -p "$PRESERVAR/$(dirname "$d")" cp -a "$DEST/$d" "$PRESERVAR/$d" ok "Preservado: $d" fi done # .venv sai tambΓ©m: dependΓͺncia de versΓ£o antiga pode conflitar com a nova. rm -rf "$DEST" mkdir -p "$DEST" if [ -n "$(ls -A "$PRESERVAR" 2>/dev/null)" ]; then cp -a "$PRESERVAR/." "$DEST/" fi rm -rf "$PRESERVAR" fi # ── 1. Entrada do usuΓ‘rio ────────────────────────────────────────────────── echo log "[1/8] ConfiguraΓ§Γ£o" # Precisa de um terminal de verdade pra ler senha sem eco. Num pipe # (curl | bash) o stdin Γ© o script, entΓ£o lemos de /dev/tty. if [ -e /dev/tty ] && [ -r /dev/tty ]; then TTY_OK=1 else TTY_OK=0 aviso "Sem terminal interativo β€” vou gerar a senha automaticamente." fi ler_senha() { local p1 p2 while true; do printf " Senha de acesso ao AetherOS (mΓ­n. 10 caracteres): " > /dev/tty IFS= read -rs p1 < /dev/tty; echo > /dev/tty if [ "${#p1}" -lt 10 ]; then erro "Curta demais (${#p1}). MΓ­nimo 10." continue fi printf " Repita a senha: " > /dev/tty IFS= read -rs p2 < /dev/tty; echo > /dev/tty if [ "$p1" != "$p2" ]; then erro "As senhas nΓ£o batem. De novo." continue fi APP_PASSWORD="$p1" return done } SENHA_GERADA=0 if [ -n "${APP_PASSWORD:-}" ]; then [ "${#APP_PASSWORD}" -ge 10 ] || morrer "APP_PASSWORD do ambiente tem menos de 10 caracteres." ok "Senha lida da variΓ‘vel de ambiente." elif [ "$TTY_OK" = "1" ]; then ler_senha ok "Senha definida." else APP_PASSWORD="$(head -c 18 /dev/urandom | base64 | tr -d '/+=' | head -c 20)" SENHA_GERADA=1 ok "Senha gerada (aparece no fim da instalaΓ§Γ£o)." fi if [ -z "${GOOGLE_API_KEY:-}" ] && [ "$TTY_OK" = "1" ]; then printf " Chave da API do Google (Gemini) β€” Enter pra configurar depois: " > /dev/tty IFS= read -rs GOOGLE_API_KEY < /dev/tty; echo > /dev/tty fi if [ -n "${GOOGLE_API_KEY:-}" ]; then ok "Chave de API recebida." else GOOGLE_API_KEY="" aviso "Sem chave de API β€” o app sobe, mas o agente nΓ£o responde atΓ© vocΓͺ" aviso "preencher GOOGLE_API_KEY em $DEST/.env" fi # JWT_SECRET nunca Γ© digitado: Γ© uma chave de assinatura, nΓ£o uma senha. Alta # entropia gerada localmente e que nunca sai desta mΓ‘quina. JWT_SECRET="$(head -c 48 /dev/urandom | base64 | tr -d '\n/+=' | head -c 64)" ok "JWT_SECRET gerado (64 caracteres)." # ── 2. DependΓͺncias ─────────────────────────────────────────────────────── echo log "[2/8] DependΓͺncias do sistema" export DEBIAN_FRONTEND=noninteractive apt-get update -qq # Sem ffmpeg e sem faster-whisper: a voz vem desligada (ENABLE_VOICE=false). apt-get install -y -qq \ python3 python3-venv python3-pip \ curl ca-certificates tar \ build-essential \ docker.io >/dev/null ok "Python $(python3 --version | cut -d' ' -f2), Docker, build-essential" systemctl enable --now docker >/dev/null 2>&1 || aviso "Docker nΓ£o subiu β€” a memΓ³ria semΓ’ntica pode nΓ£o funcionar." # ── 3. Download ─────────────────────────────────────────────────────────── echo log "[3/8] Baixando o AetherOS" TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT curl -fsSL "$BASE_URL/aetheros-latest.tar.gz" -o "$TMP/aetheros.tar.gz" \ || morrer "Falha ao baixar o pacote de $BASE_URL" # VerificaΓ§Γ£o de integridade: sem isso, um pacote corrompido no meio do # download sΓ³ apareceria como erro estranho de Python muito mais tarde. if curl -fsSL "$BASE_URL/aetheros-latest.tar.gz.sha256" -o "$TMP/esperado.sha256" 2>/dev/null; then ESPERADO="$(tr -d ' \n-' < "$TMP/esperado.sha256")" OBTIDO="$(sha256sum "$TMP/aetheros.tar.gz" | cut -d' ' -f1)" [ "$ESPERADO" = "$OBTIDO" ] || morrer "Checksum nΓ£o bate. Pacote corrompido ou alterado β€” abortando." ok "Checksum verificado." else aviso "NΓ£o achei o arquivo de checksum β€” seguindo sem verificar." fi mkdir -p "$DEST" tar -xzf "$TMP/aetheros.tar.gz" -C "$TMP" cp -a "$TMP/aetheros/." "$DEST/" [ -f "$DEST/backend/app/main.py" ] || morrer "Pacote sem backend/app/main.py β€” nΓ£o Γ© um release vΓ‘lido." ok "ExtraΓ­do em $DEST (versΓ£o $(cat "$DEST/VERSION" 2>/dev/null || echo '?'))" # ── 4. Python ───────────────────────────────────────────────────────────── echo log "[4/8] Ambiente Python (leva alguns minutos)" python3 -m venv "$DEST/.venv" PIP="$DEST/.venv/bin/pip" "$PIP" install --upgrade pip -q # PyTorch CPU ANTES de tudo. sentence-transformers (embeddings da memΓ³ria # semΓ’ntica) depende de torch, e o wheel padrΓ£o do PyPI vem com o stack CUDA # inteiro: ~2.7GB de bibliotecas nvidia + 1.2GB de torch + 691MB de triton. # Numa VM sem GPU isso Γ© 4.6GB baixados e ocupados pra nada. O Γ­ndice cpu # entrega o mesmo torch sem CUDA, em ~200MB. Instalando ele primeiro, o # resolvedor do pip encontra o requisito jΓ‘ satisfeito e nΓ£o busca o wheel # gordo depois. log " Instalando PyTorch (CPU, sem CUDA)..." "$PIP" install -q torch --index-url https://download.pytorch.org/whl/cpu \ || aviso "Falha no torch CPU β€” o pip vai resolver sozinho (pode baixar bem mais)." # faster-whisper fora: a voz estΓ‘ desligada e o modelo sΓ£o centenas de MB # pra um recurso que nΓ£o Γ© usado. grep -viE '^faster[-_]whisper' "$DEST/backend/requirements.txt" > "$TMP/req.txt" log " Instalando o resto das dependΓͺncias..." "$PIP" install -r "$TMP/req.txt" -q \ || morrer "Falha ao instalar as dependΓͺncias Python." TAM_VENV="$(du -sh "$DEST/.venv" 2>/dev/null | cut -f1)" ok "DependΓͺncias instaladas (venv: ${TAM_VENV:-?})." # ── 5. ConfiguraΓ§Γ£o ─────────────────────────────────────────────────────── echo log "[5/8] Gerando configuraΓ§Γ£o" TS_IP="$(tailscale ip -4 2>/dev/null | head -1 || true)" CORS="http://localhost:${PORTA},http://127.0.0.1:${PORTA}" [ -n "$TS_IP" ] && CORS="${CORS},http://${TS_IP}:${PORTA}" umask 077 cat > "$DEST/.env" </dev/null 2>&1; then docker rm -f aetheros-chromadb >/dev/null 2>&1 || true # Publicado sΓ³ em 127.0.0.1: o Chroma nΓ£o tem autenticaΓ§Γ£o, entΓ£o nΓ£o pode # escutar em interface alcanΓ§Γ‘vel de fora. if docker run -d --name aetheros-chromadb --restart unless-stopped \ -p 127.0.0.1:8001:8000 \ -v "$DEST/chroma_data:/chroma/chroma" \ chromadb/chroma:latest >/dev/null 2>&1; then ok "ChromaDB rodando em 127.0.0.1:8001" else aviso "ChromaDB nΓ£o subiu. O AetherOS funciona sem ele; sΓ³ a tool" aviso "buscar_memoria fica indisponΓ­vel." fi else aviso "Docker indisponΓ­vel β€” pulando o ChromaDB." fi # ── 7. ServiΓ§o ──────────────────────────────────────────────────────────── echo log "[7/8] ServiΓ§o systemd" cat > "/etc/systemd/system/${SERVICE}.service" </dev/null 2>&1 systemctl restart "$SERVICE" ok "ServiΓ§o habilitado no boot." # Firewall: a porta 8000 nΓ£o vai pra internet. Liberada sΓ³ na tailscale0. if command -v ufw >/dev/null 2>&1; then ufw allow 22/tcp >/dev/null 2>&1 || true if [ -n "$TS_IP" ]; then ufw allow in on tailscale0 to any port "$PORTA" proto tcp >/dev/null 2>&1 || true ufw deny "$PORTA/tcp" >/dev/null 2>&1 || true ok "Firewall: porta $PORTA sΓ³ pela Tailscale." else ufw deny "$PORTA/tcp" >/dev/null 2>&1 || true aviso "Tailscale nΓ£o detectada β€” porta $PORTA bloqueada de fora." aviso "Instale a Tailscale (tailscale up) e rode:" aviso " ufw allow in on tailscale0 to any port $PORTA proto tcp" fi else aviso "ufw nΓ£o instalado β€” a porta $PORTA pode estar exposta. Cuidado." fi # ── 8. VerificaΓ§Γ£o ──────────────────────────────────────────────────────── # A parte que mais importa: nΓ£o anunciar sucesso sem provar que subiu. Um # instalador que diz "instalado com sucesso" sem checar Γ© pior que um que # falha, porque esconde o problema. echo log "[8/8] Verificando a instalaΓ§Γ£o" subiu=0 for i in $(seq 1 40); do if curl -fsS -o /dev/null "http://127.0.0.1:${PORTA}/" 2>/dev/null; then subiu=1; break fi sleep 2 done if [ "$subiu" != "1" ]; then erro "O serviΓ§o nΓ£o respondeu na porta $PORTA depois de 80s." echo echo "Últimas linhas do log:" journalctl -u "$SERVICE" -n 25 --no-pager || true echo erro "InstalaΓ§Γ£o NΓƒO concluΓ­da. O serviΓ§o estΓ‘ instalado mas nΓ£o estΓ‘ de pΓ©." exit 1 fi ok "HTTP respondendo na porta $PORTA." # A senha funciona? E uma senha errada Γ© recusada? Sem os dois, "tem senha" # nΓ£o significa nada. cod_ok="$(curl -s -o /dev/null -w '%{http_code}' -X POST \ "http://127.0.0.1:${PORTA}/api/auth/login" \ -H 'Content-Type: application/json' \ --data-binary "$(printf '{"password":"%s"}' "$APP_PASSWORD")" || echo 000)" cod_ruim="$(curl -s -o /dev/null -w '%{http_code}' -X POST \ "http://127.0.0.1:${PORTA}/api/auth/login" \ -H 'Content-Type: application/json' \ --data-binary '{"password":"senha-obviamente-errada"}' || echo 000)" if [ "$cod_ok" = "200" ]; then ok "Login com a sua senha: aceito." else erro "Login com a senha configurada falhou (HTTP $cod_ok)." exit 1 fi if [ "$cod_ruim" = "401" ] || [ "$cod_ruim" = "429" ]; then ok "Login com senha errada: recusado." else erro "Senha errada NΓƒO foi recusada (HTTP $cod_ruim) β€” autenticaΓ§Γ£o furada." exit 1 fi # ── Resumo ──────────────────────────────────────────────────────────────── echo echo -e "${VERDE}══════════════════════════════════════════════════════════${NC}" echo -e "${VERDE} ✨ AetherOS instalado e verificado${NC}" echo -e "${VERDE}══════════════════════════════════════════════════════════${NC}" echo if [ -n "$TS_IP" ]; then echo -e " Acesse: ${AZUL}http://${TS_IP}:${PORTA}${NC} ${CINZA}(pela Tailscale)${NC}" else echo -e " Acesse: ${AMARELO}Tailscale nΓ£o configurada${NC}" echo -e " Rode ${AZUL}tailscale up${NC} e acesse http://:${PORTA}" fi if [ "$SENHA_GERADA" = "1" ]; then echo echo -e " ${AMARELO}Senha gerada (anote agora, nΓ£o aparece de novo):${NC}" echo -e " ${VERDE}${APP_PASSWORD}${NC}" fi if [ -z "$GOOGLE_API_KEY" ]; then echo echo -e " ${AMARELO}Falta a chave de API:${NC} edite GOOGLE_API_KEY em $DEST/.env" echo -e " e rode ${AZUL}systemctl restart ${SERVICE}${NC}" fi echo echo -e " ${CINZA}Status: systemctl status ${SERVICE}${NC}" echo -e " ${CINZA}Logs: journalctl -u ${SERVICE} -f${NC}" echo -e " ${CINZA}Config: ${DEST}/.env${NC}" echo -e " ${CINZA}Arquivos: ${DEST}/workspace${NC}" echo