# 04 — Проверяемая пользовательская установка Xray **What to build:** Пользователь без root устанавливает официальный Xray одной явной командой `install` и получает проверенный бинарник зафиксированной версии, без скрытой замены существующей установки, без записи за пределами пользовательских каталогов и без риска для живого managed-экземпляра. **Prerequisites:** 01, 03, 06 — все done. Наследуемые решения: одна поддержанная версия Xray 26.3.27 (06); идентичность managed-процессов по `/proc/PID/exe` и start-lock (03); Python 3.8+ stdlib-only, переносимость Ubuntu / РЕД ОС / Astra / Alt. **Status:** done ## Зафиксированный протокол установки - **Только версия 26.3.27** — та же, что принимает `VERSION_LINE`. Никакого параметра версии: будущий апгрейд — отдельный тикет, меняющий пин скачивания, ворота рантайма, фикстуры и документацию вместе. - **Источник:** `https://github.com/XTLS/Xray-core/releases/download/v26.3.27/`. Маппинг архитектур (чистая функция, по `platform.machine()`): `x86_64`/`amd64` → `Xray-linux-64.zip`; `aarch64`/`arm64` → `Xray-linux-arm64-v8a.zip`; прочее — понятный отказ. - **Проверка:** скачивается `<архив>.dgst`. Фактический формат официальных файлов — четыре записи `MD5=`, `SHA1=`, `SHA2-256=`, `SHA2-512=` (исправлено 2026-07-20 по находке CODE-ревью: однострочного `SHA256=` в релизах нет). Принимается ровно одна запись `SHA2-256= <64 hex в нижнем регистре>`; дубликаты, uppercase, отсутствие записи или мусор — отказ. Хэш архива считается `hashlib.sha256`; несовпадение или неожиданный формат — отказ до распаковки, установленный бинарник не тронут. - **Границы размеров:** скачивание и распаковка ограничены зафиксированными лимитами (архив, `.dgst`, извлекаемый бинарник); фактически прочитанные байты считаются с limit+1; несоответствие объявленному `Content-Length` или превышение лимита — отказ без исчерпания диска/памяти. - **Инструменты:** `urllib.request` для скачивания, `zipfile` для извлечения члена `xray`. Явно исключены: tar, GPG-подписи, curl, wget, пакетные менеджеры, sudo, systemd, системные каталоги. - **Атомарность:** приватный staging-каталог создаётся ВНУТРИ `~/.local/bin` (одна файловая система — `os.replace` без EXDEV); скачивание и распаковка туда; кандидату ставится режим 0755; кандидат проходит проверку `version` (те же ворота 26.3.27); затем один `os.replace` в `~/.local/bin/xray`. До этого вызова существующий файл назначения не изменяется. Staging убирается при любой ошибке и прерывании (best-effort); fsync/power-loss-гарантии не требуются. ## Чекбоксы - [x] Команда `install [--replace]` добавлена в парсер и диспетчеризуется ДО `load_config()`: установка не требует машинного конфига и VLESS-профиля. `start` и запуск без подкоманды по-прежнему никогда не начинают скачивание. - [x] Защита managed-режима: до любого скачивания и изменения назначения `install` берёт существующий start-lock и держит его до конца `os.replace`. Lock занят — немедленный отказ с подсказкой «сначала stop». При взятом lock допускаются только отсутствующий state или доказуемо устаревший (по классификации 03 — stale); живой, сиротский или непроверяемый state — отказ. - [x] Поведение при существующем назначении `~/.local/bin/xray`: - отсутствует → установка; - обычный исполняемый файл, отвечающий `26.3.27` → no-op без сети (успех, сообщение «уже установлено»); - `--replace` → принудительная переустановка с полной проверкой; - иной файл (не отвечает 26.3.27, неисполняемый, битый) или симлинк → отказ без `--replace`; с `--replace` заменяется сам путь, симлинк не разыменовывается; - каталог → всегда понятный отказ. - [x] После установки: проверка `shutil.which("xray")` — резолвится ли в установленный `~/.local/bin/xray`. Нет — ненулевой код с подсказкой добавить `~/.local/bin` в начало `PATH`; готовая установка при этом НЕ откатывается. - [x] Тестовый шов: чистая функция «архитектура → имя ассета» и подменяемый загрузчик (callable), который тесты заменяют in-process. Пользовательского оверрайда базового URL нет. - [x] Автоматические тесты (без root, без сети; временные HOME/PATH/state): - маппинг обеих архитектур (patch значения архитектуры) и отказ на неподдержанной; - корректный `.dgst` → установка; повреждённый хэш и неожиданный формат `.dgst` → отказ до распаковки, назначение не тронуто; - фикстуры: локальный ZIP с фейковым исполняемым, печатающим `Xray 26.3.27 ...`, и парные валидный/битый `.dgst`; - существующая корректная установка → no-op без обращения к загрузчику; `--replace` → переустановка; симлинк/каталог/битый файл — по таблице выше; - атомарность: инъецированная ошибка после скачивания / при распаковке / при проверке `version` → назначение не изменено, staging убран; - запись только в пределах staging/назначения и lock-файла: сверка полного дерева временной ФС до/после; - защита managed: при живом managed-экземпляре (реальная фикстура 03) `install --replace` отказывает с подсказкой «сначала stop», бинарник и state не тронуты; - `shutil.which`-проверка: отсутствие каталога в PATH и конкурирующий более ранний `xray` в PATH. - [x] `proxy-client/README.md` обновлён: одна поддержанная версия 26.3.27 (упоминания 25.12.8 удалены), использование `install [--replace]`, маппинг x86_64/aarch64, SHA-256-проверка, поведение PATH, отказ при живом managed-экземпляре. ## Сохранить, не трогать - Существующее поведение `resolve_xray`, ворот `VERSION_LINE`, managed-режима и всех 49 зелёных тестов + 8 shell-проверок. - Секреты: install не касается профилей и не выводит их; общее правило «VLESS/UUID/ключи никогда в stdout/stderr/argv» остаётся. - `bash/.bash_functions`, установщик dotfiles — вне объёма. - Никаких коммитов. ## Отчёт исполнителя 1. Изменённые файлы со строкой «зачем» на каждый. 2. Команды тестов и счётчики (baseline: 49 OK / 8 PASS — новых станет больше, указать числа). 3. Чекбоксы: закрыт / не закрыт и почему. 4. Принятые по ходу решения и сомнения, если есть. ## Резолюция (2026-07-20) Конвейер пройден целиком: постановка → Codex spec-review (high, 5 MAJOR + 3 MINOR + 1 NIT, переписана) → реализация (Codex, medium — эксперимент) → саморевью (found 6, fixed 6) → два слепых ревью (CODE: 2 MAJOR + 2 MINOR; TASK: 3 NIT) → триаж (FIXED 7, REJECTED 0) → узкие перепроверки (оба FIXES_CONFIRMED) → приёмка оркестратором. Ключевое: - Команда `install [--replace]`: пин 26.3.27, маппинг архитектур чистой функцией, `urllib.request` + `zipfile`, staging внутри `~/.local/bin`, один `os.replace`, `version`-ворота кандидата, guard по start-lock. - CODE-1 (MAJOR) оказался дефектом постановки: spec-review зафиксировал несуществующий однострочный формат `SHA256=`; реальные `.dgst` — четыре записи. Протокол в тикете исправлен (см. выше), парсер строгий: ровно одна `SHA2-256= <64 hex в нижнем регистре>`. - Границы размеров: 64 MiB архив/бинарник, 4 KiB `.dgst`, счёт limit+1, сверка Content-Length; offline-покрытие реального `download_file` через подмену `urllib.request.urlopen`. - `StartLockBusyError` вместо парсинга текста ошибки; снапшоты дерева ФС в тестах отказных путей; PATH-тест конкурирующего бинарника. - README дополнён (по просьбе пользователя): последняя версия с `allowInsecure` — 25.12.8, опция удалена в 26.2.6; CLI их не ставит. Тесты: 79 OK (baseline 49, +30) + 8 PASS shell-проверок. Затраты — в `.scratch/proxy-client/token-ledger.md`.