#!/usr/bin/env sh # Установка CapyAgent одной строкой. # # curl -fsSL https://capyagent.ru/install.sh | sh # # 🔴 На минимальном Debian или Ubuntu (контейнер, свежий VPS) нет ни curl, ни # корневых сертификатов, и строка выше падает раньше, чем доходит до этого # скрипта: «curl: not found». Сначала одна команда от root: # # apt-get update && apt-get install -y curl ca-certificates # # 🔴 Скажем прямо, что тут происходит: вы запускаете у себя то, что вам прислал # наш сервер. Это удобно и это доверие к нам и к TLS - не больше и не меньше. # Если так не хотите (и правильно, если не хотите): # # curl -fsSLO https://capyagent.ru/install.sh # less install.sh # прочитать # sh install.sh # # 🔴 Ставит В ДОМАШНИЙ КАТАЛОГ и БЕЗ sudo. Службой на весь сервер - отдельным # шагом и осознанно: программа получает доступ к вашим системам, и решение # «пусть работает всегда от имени машины» человек принимает отдельно от # решения «дай посмотреть». set -eu # 🔴 Файлы берутся с нашего канала выпуска capyagent.ru/dl (решение владельца # 05.09). Выпуски GitHub остаются архивом версий, но репозиторий закрыт, и # постороннему они отвечают 404 - неотличимо от «выпуска нет». BAZA="${CAPYAGENT_BASE:-https://capyagent.ru/dl/latest}" KUDA="${CAPYAGENT_BIN:-$HOME/.local/bin}" skazat() { printf '%s\n' "$*"; } upast() { printf '🔴 %s\n' "$*" >&2; exit 1; } # ---------- какая машина ---------- OS=$(uname -s | tr '[:upper:]' '[:lower:]') case "$(uname -m)" in x86_64|amd64) ARCH=amd64 ;; aarch64|arm64) ARCH=arm64 ;; *) upast "Не знаю сборки под $(uname -m). Есть linux amd64/arm64 и macOS arm64." ;; esac case "$OS" in linux|darwin) : ;; *) upast "Не знаю сборки под $OS." ;; esac IMYA="capyagent-$OS-$ARCH" # ---------- чем качать ---------- if command -v curl >/dev/null 2>&1; then vzyat() { curl -fsSL "$1"; } vzyat_v() { curl -fsSL "$1" -o "$2"; } elif command -v wget >/dev/null 2>&1; then vzyat() { wget -qO- "$1"; } vzyat_v() { wget -qO "$2" "$1"; } else upast "Нет ни curl, ни wget - нечем скачать." fi # 🔴 Сумму проверяем обязательно, и без чем считать не ставим. «Поставлю без # проверки» здесь означало бы: возьму у сети исполняемый файл и запущу, не # посмотрев. Отказ честнее. if command -v sha256sum >/dev/null 2>&1; then summa() { sha256sum "$1" | awk '{print $1}'; } elif command -v shasum >/dev/null 2>&1; then summa() { shasum -a 256 "$1" | awk '{print $1}'; } else upast "Нет ни sha256sum, ни shasum - нечем проверить, что файл дошёл целым." fi skazat "CapyAgent: ставлю $IMYA" skazat " откуда: $BAZA" skazat " куда: $KUDA" # ---------- что выпущено ---------- VREM=$(mktemp -d) KUDA_CONNECTORS="" KUDA_CONNECTORS_OLD="" KUDA_CONNECTORS_STAGE="" KUDA_BINARY_STAGE="" KUDA_BINARY_OLD="" CONNECTORS_PLACED=0 BINARY_PLACED=0 INSTALL_COMMITTED=0 cleanup() { code=$? trap - EXIT HUP INT TERM if [ "$INSTALL_COMMITTED" -ne 1 ]; then if [ "$BINARY_PLACED" -eq 1 ]; then rm -f "$KUDA/capyagent"; fi if [ -n "$KUDA_BINARY_OLD" ] && [ -e "$KUDA_BINARY_OLD" ]; then mv "$KUDA_BINARY_OLD" "$KUDA/capyagent" 2>/dev/null || true fi if [ "$CONNECTORS_PLACED" -eq 1 ]; then rm -rf "$KUDA_CONNECTORS"; fi if [ -n "$KUDA_CONNECTORS_OLD" ] && [ -e "$KUDA_CONNECTORS_OLD" ]; then mv "$KUDA_CONNECTORS_OLD" "$KUDA_CONNECTORS" 2>/dev/null || true fi fi [ -n "$KUDA_CONNECTORS_STAGE" ] && rm -rf "$KUDA_CONNECTORS_STAGE" [ -n "$KUDA_BINARY_STAGE" ] && rm -f "$KUDA_BINARY_STAGE" rm -rf "$VREM" exit "$code" } trap cleanup EXIT trap 'exit 1' HUP INT TERM # 🔴 Называем самую вероятную причину отказа. Человек, читающий «проверьте # сеть», будет чинить сеть, даже если дело в сертификатах. vzyat "$BAZA/checksums.txt" > "$VREM/checksums.txt" 2>/dev/null \ || upast "Не забрать список сумм с $BAZA. Вероятные причины по убыванию: 1) нет сети или закрыт доступ к $(printf '%s' "$BAZA" | sed 's#^[a-z]*://##; s#/.*##'); 2) нет корневых сертификатов (минимальный Debian/Ubuntu): apt-get install -y ca-certificates; 3) свой адрес задан неверно через CAPYAGENT_BASE." # Ищем нашу строку. Пути в файле записаны как bin/<имя>. OZHID=$(grep "/$IMYA\$" "$VREM/checksums.txt" | awk '{print $1}' | head -1 || true) [ -n "$OZHID" ] || upast "В выпуске нет сборки под $OS/$ARCH." skazat " качаю..." # 🔴 Два адреса, и порядок не случаен. GitHub Releases хранит вложения # ПЛОСКО: имя вложения не может содержать косую черту, и «.../download/bin/имя» # отвечает 404 всегда - хотя в checksums.txt пути записаны с bin/. Свой сервер # отдаёт каталог выпуска как есть: файл лежит в bin/, а рядом, в корне, та же # жёсткая ссылка под плоским именем для старых клиентов (build-release.sh). # Пробуем плоское имя, затем bin/ - отказ только после обоих. vzyat_v "$BAZA/$IMYA" "$VREM/$IMYA" 2>/dev/null \ || vzyat_v "$BAZA/bin/$IMYA" "$VREM/$IMYA" 2>/dev/null \ || upast "Не скачать бинарник: ни $BAZA/$IMYA, ни $BAZA/bin/$IMYA не отвечает." NASHLA=$(summa "$VREM/$IMYA") [ "$NASHLA" = "$OZHID" ] || { printf '🔴 Контрольная сумма не сошлась.\n' >&2 printf ' пришло: %s\n' "$NASHLA" >&2 printf ' ожидалось: %s\n' "$OZHID" >&2 printf ' Файл по дороге изменился - запускать его нельзя.\n' >&2 exit 1 } skazat " сумма сошлась" # ---------- встроенные MCP-коннекторы ---------- # Однострочная установка кладёт бинарник отдельно; без этого архива Python # запустился бы с путём connectors/... которого у человека нет, и MCP отвечал # бы EOF. Архив и его размер/сумму описывает тот же манифест выпуска, который # использует обновление ядра. vzyat "$BAZA/manifest.json" > "$VREM/manifest.json" 2>/dev/null \ || upast "Не забрать манифест выпуска с $BAZA; бинарник не установлен." manifest_field() { awk -F'"' -v name="$1" '$2 == name { print $4; exit }' "$VREM/manifest.json" } KONN_VERSION=$(manifest_field version) KONN_FILE=$(manifest_field connectors_file) KONN_SHA=$(manifest_field connectors_sha256) KONN_SIZE=$(awk -F: '/"connectors_size"/ { gsub(/[^0-9]/, "", $2); print $2; exit }' "$VREM/manifest.json") case "$KONN_VERSION" in ''|*[!A-Za-z0-9._+-]*) upast "В манифесте некорректная версия набора коннекторов." ;; esac [ "$KONN_FILE" = "connectors-$KONN_VERSION.tar.gz" ] \ || upast "В манифесте некорректное имя архива коннекторов." [ "${#KONN_SHA}" -eq 64 ] \ || upast "В манифесте некорректная сумма архива коннекторов." case "$KONN_SHA" in *[!0123456789abcdefABCDEF]*) upast "В манифесте некорректная сумма архива коннекторов." ;; esac case "$KONN_SIZE" in ''|*[!0-9]*) upast "В манифесте некорректный размер архива коннекторов." ;; esac [ "$KONN_SIZE" -gt 0 ] || upast "В манифесте пустой архив коннекторов." vzyat_v "$BAZA/$KONN_FILE" "$VREM/$KONN_FILE" 2>/dev/null \ || upast "Не скачать архив встроенных коннекторов; бинарник не установлен." KONN_GOT_SIZE=$(wc -c < "$VREM/$KONN_FILE" | tr -d '[:space:]') [ "$KONN_GOT_SIZE" = "$KONN_SIZE" ] \ || upast "Размер архива встроенных коннекторов не сошёлся; бинарник не установлен." KONN_GOT_SHA=$(summa "$VREM/$KONN_FILE") [ "$KONN_GOT_SHA" = "$KONN_SHA" ] \ || upast "Сумма архива встроенных коннекторов не сошлась; бинарник не установлен." tar -tzf "$VREM/$KONN_FILE" > "$VREM/connectors.list" 2>/dev/null \ || upast "Архив встроенных коннекторов повреждён; бинарник не установлен." while IFS= read -r member; do case "$member" in connectors|connectors/*) : ;; *) upast "В архиве коннекторов неожиданное содержимое; бинарник не установлен." ;; esac case "$member" in /*|../*|*/../*|*/..|..) upast "В архиве коннекторов небезопасный путь; бинарник не установлен." ;; esac done < "$VREM/connectors.list" tar -tvzf "$VREM/$KONN_FILE" > "$VREM/connectors.verbose" 2>/dev/null \ || upast "Не проверить состав архива коннекторов; бинарник не установлен." awk 'substr($1, 1, 1) !~ /^[-d]$/ { bad=1 } END { exit bad }' "$VREM/connectors.verbose" \ || upast "В архиве коннекторов есть ссылка или специальный файл; бинарник не установлен." mkdir -p "$VREM/connectors-stage" tar -xzf "$VREM/$KONN_FILE" -C "$VREM/connectors-stage" --no-same-owner \ || upast "Не распаковать архив встроенных коннекторов; бинарник не установлен." [ -d "$VREM/connectors-stage/connectors" ] \ || upast "В архиве нет каталога connectors/; бинарник не установлен." if ! find "$VREM/connectors-stage/connectors" -type f -print | grep -q .; then upast "В архиве нет файлов коннекторов; бинарник не установлен." fi mkdir -p "$KUDA" KONN_ROOT="${CAPYAGENT_CONNECTORS_DIR:-$KUDA}" case "${KONN_ROOT##*/}" in connectors) KUDA_CONNECTORS="$KONN_ROOT" ;; *) KUDA_CONNECTORS="$KONN_ROOT/connectors" ;; esac KONN_PARENT=$(dirname "$KUDA_CONNECTORS") mkdir -p "$KONN_PARENT" KUDA_CONNECTORS_OLD="$KONN_PARENT/.connectors.old.$$" KUDA_CONNECTORS_STAGE="$KONN_PARENT/.connectors.new.$$" KUDA_BINARY_STAGE="$KUDA/.capyagent.new.$$" KUDA_BINARY_OLD="$KUDA/.capyagent.old.$$" [ ! -e "$KUDA_CONNECTORS_OLD" ] && [ ! -L "$KUDA_CONNECTORS_OLD" ] \ && [ ! -e "$KUDA_CONNECTORS_STAGE" ] && [ ! -L "$KUDA_CONNECTORS_STAGE" ] \ && [ ! -e "$KUDA_BINARY_STAGE" ] && [ ! -L "$KUDA_BINARY_STAGE" ] \ && [ ! -e "$KUDA_BINARY_OLD" ] && [ ! -L "$KUDA_BINARY_OLD" ] \ || upast "В каталоге установки остались промежуточные файлы; сначала проверьте $KUDA." if { [ -e "$KUDA_CONNECTORS" ] || [ -L "$KUDA_CONNECTORS" ]; } && [ ! -d "$KUDA_CONNECTORS" ]; then upast "На месте каталога connectors уже есть не каталог; прежняя установка сохранена." fi mkdir "$KUDA_CONNECTORS_STAGE" if [ -d "$KUDA_CONNECTORS" ]; then if find "$KUDA_CONNECTORS" ! -type f ! -type d -print | grep -q .; then upast "В каталоге connectors есть ссылка или специальный файл; прежняя установка сохранена." fi cp -R "$KUDA_CONNECTORS/." "$KUDA_CONNECTORS_STAGE/" \ || upast "Не сохранить прежние файлы коннекторов; прежняя установка сохранена." fi cp -R "$VREM/connectors-stage/connectors/." "$KUDA_CONNECTORS_STAGE/" \ || upast "Не подготовить файлы встроенных коннекторов; прежняя установка сохранена." chmod 755 "$VREM/$IMYA" cp "$VREM/$IMYA" "$KUDA_BINARY_STAGE" \ || upast "Не подготовить бинарник; прежняя установка сохранена." chmod 755 "$KUDA_BINARY_STAGE" if [ -e "$KUDA_CONNECTORS" ] || [ -L "$KUDA_CONNECTORS" ]; then mv "$KUDA_CONNECTORS" "$KUDA_CONNECTORS_OLD" \ || upast "Не отложить прежние коннекторы; установка не изменена." fi if ! mv "$KUDA_CONNECTORS_STAGE" "$KUDA_CONNECTORS"; then [ ! -e "$KUDA_CONNECTORS_OLD" ] || mv "$KUDA_CONNECTORS_OLD" "$KUDA_CONNECTORS" || true upast "Не установить файлы коннекторов; прежняя установка сохранена." fi CONNECTORS_PLACED=1 if [ -e "$KUDA/capyagent" ] || [ -L "$KUDA/capyagent" ]; then if [ ! -f "$KUDA/capyagent" ] && [ ! -L "$KUDA/capyagent" ]; then upast "На месте бинарника capyagent уже есть не файл; прежняя установка сохранена." fi mv "$KUDA/capyagent" "$KUDA_BINARY_OLD" \ || upast "Не отложить прежний бинарник; установка не изменена." fi if ! mv "$KUDA_BINARY_STAGE" "$KUDA/capyagent"; then [ ! -e "$KUDA_BINARY_OLD" ] || mv "$KUDA_BINARY_OLD" "$KUDA/capyagent" || true upast "Не установить бинарник; прежняя установка сохранена." fi BINARY_PLACED=1 INSTALL_COMMITTED=1 rm -rf "$KUDA_CONNECTORS_OLD" rm -f "$KUDA_BINARY_OLD" # ---------- виден ли он ---------- # 🔴 Каталог с программой дописываем в PATH САМИ (02.10, первый запуск # новичка): строка «добавьте в ~/.profile» оставляла человека без команды # capyagent, а мастер в конце советует именно «capyagent chat». У root даже # вход через login shell не находил ~/.local/bin. Пишем в ~/.profile и в файл # своей оболочки, один раз (повтор установки строку не множит) и одной строкой # говорим об этом. Блок между метками исполняет тест установщика отдельно. # >>> put-v-path # dopisat_put ФАЙЛ СТРОКА - строка в файле ровно один раз; успех, если она там # есть. Отказ оболочки («cannot create», «Is a directory») молчит: о том, что # записать не вышло, говорим своими словами ниже. dopisat_put() { if [ -f "$1" ] && grep -qF "$2" "$1" 2>/dev/null; then return 0 fi { if [ -s "$1" ] && [ -n "$(tail -c1 "$1")" ]; then printf '\n' >> "$1" || return 1 fi printf '%s\n' "$2" >> "$1" } 2>/dev/null } if ! command -v capyagent >/dev/null 2>&1; then PUT_STROKA="export PATH=\"\$PATH:$KUDA\" # capyagent" PUT_ZAPISANO="" PUT_FAYLY=".profile" PUT_OBOLOCHKA=$(basename "${SHELL:-sh}") case "$PUT_OBOLOCHKA" in zsh) PUT_FAYLY="$PUT_FAYLY .zshrc" ;; bash) PUT_FAYLY="$PUT_FAYLY .bashrc" ;; esac [ -f "$HOME/.bash_profile" ] && PUT_FAYLY="$PUT_FAYLY .bash_profile" # Имена файлов без пробелов, поэтому разбиение по словам здесь безопасно; # домашний каталог с пробелом стоит в кавычках. for PUT_FAYL in $PUT_FAYLY; do dopisat_put "$HOME/$PUT_FAYL" "$PUT_STROKA" && PUT_ZAPISANO="$PUT_ZAPISANO ~/$PUT_FAYL" done # 🔴 fish не читает ни ~/.profile, ни ~/.bashrc, а POSIX export в его # конфиге - ошибка разбора. Для fish своя строка его синтаксисом в conf.d # (fish читает этот каталог сам, config.fish не трогаем). Путь с ' или \ в # строку fish не кладём: проще сказать словами. PUT_FISH="" if [ "$PUT_OBOLOCHKA" = fish ]; then PUT_FISH=" (fish: fish_add_path $KUDA)" case "$KUDA" in *\'*|*\\*) : ;; *) PUT_FISH_KAT="${XDG_CONFIG_HOME:-$HOME/.config}/fish/conf.d" if mkdir -p "$PUT_FISH_KAT" 2>/dev/null && dopisat_put "$PUT_FISH_KAT/capyagent.fish" "contains -- '$KUDA' \$PATH; or set -gx PATH \$PATH '$KUDA' # capyagent"; then PUT_ZAPISANO="$PUT_ZAPISANO $PUT_FISH_KAT/capyagent.fish" PUT_FISH="" fi ;; esac fi skazat "" if [ -n "$PUT_ZAPISANO" ]; then skazat "PATH: дописал $KUDA в$PUT_ZAPISANO; в этом окне зовите $KUDA/capyagent, в новом - просто capyagent$PUT_FISH." else skazat "🔴 $KUDA не в вашем PATH, и записать его не вышло. Добавьте строку в ~/.profile:" skazat " export PATH=\"\$PATH:$KUDA\"" skazat " Или зовите по полному пути: $KUDA/capyagent$PUT_FISH" fi fi # <<< put-v-path skazat "" skazat "Поставлено: $("$KUDA/capyagent" version 2>/dev/null || echo "$KUDA/capyagent")" skazat "" # 🔴 Мастер запускаем САМИ, если человек сидит за терминалом. # # Установка, которая заканчивается словами «а теперь наберите ещё команду», - # это лишняя ступенька, и на ней половина уходит. У соседей мастер открывается # сам, и это не украшение: человек, поставивший программу одной строкой, ждёт, # что дальше с ним заговорят. # # 🔴 Проверяем /dev/tty, а не стандартный ввод: при `curl ... | sh` ввод занят # самим скриптом, и мастер, смотрящий только на него, откажется работать. # Проверяем ОТКРЫТИЕМ, а не правами: в контейнере без терминала /dev/tty # лежит с правами на чтение, но не открывается, и `[ -r /dev/tty ]` пускал # сюда, чтобы оболочка напечатала «cannot open /dev/tty» вместо нашей # подсказки. # # 🔴 Файл настроек мастер выбирает сам, и выбирает тот, который читает # программа: без службы - файл каталога данных (`capyagent config path`), а # если человек согласится запустить агента службой (мастер предлагает это, # когда настроен бот), - файл окружения службы. Называть ему файл отсюда # нельзя: до 01.10 скрипт передавал --файл каталога данных, и root, выбравший # службу, получал настройки там, куда системная служба не смотрит. # # 🔴 Провал мастера - не «ничего не записано»: он мог записать настройки и не # запустить службу (модель не поднялась, бот не ответил). Что именно не так, # мастер пишет сам строкой выше. if [ -n "${CAPYAGENT_SETUP_ANSWERS:-}" ]; then # Файл ответов: вопросов нет, setup берёт путь из переменной сам. "$KUDA/capyagent" setup || upast "Настройка по файлу ответов не закончена: причина выше." elif ( : /dev/null; then skazat "Настраиваю. Спрошу про модель и проверю её живьём." skazat "" "$KUDA/capyagent" setup < /dev/tty || { skazat "" skazat "Мастер не закончил: что не так, написано выше. Продолжить, когда удобно:" skazat " $KUDA/capyagent setup" } else skazat "Дальше одна команда - она спросит про модель, проверит её живьём" skazat "и, если настроите бота, запустит агента службой:" skazat " $KUDA/capyagent setup" fi skazat "" skazat "🔴 Что программа умеет и куда ходит - покажет она сама:" skazat " $KUDA/capyagent doctor --prove"