feat(xray-proxy): добавлена проверяемая установка Xray командой install

- Зачем:
  - пользователь без root должен получать проверенный бинарник Xray 26.3.27
    одной явной командой, без риска для живого managed-экземпляра.
- Что:
  - команда install [--replace]: пин 26.3.27, маппинг x86_64/aarch64,
    скачивание urllib + проверка SHA2-256 из официального четырёхзаписного
    .dgst, staging внутри ~/.local/bin и один атомарный os.replace;
  - границы размеров (64 MiB архив/бинарник, 4 KiB .dgst, счёт limit+1,
    сверка Content-Length) и guard managed-режима через start-lock;
  - типизированная StartLockBusyError, offline-тесты реального загрузчика
    через подмену urlopen, снапшоты дерева ФС в отказных путях;
  - README/спека обновлены, включая справку о последней версии с
    allowInsecure (25.12.8, удалена в 26.2.6); тикет 04 закрыт, леджер
    дополнен итогами эксперимента.
- Проверка:
  - PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s tests — 79 OK.
  - bash tests/test_proxy_launchers.sh — 8 PASS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
ddadminandClaude Fable 5 committed 2026-07-20 13:46:33 -04:00
1 parent 90cc8456cd
commit 0432c4ae8a
7 files changed
+1266 -43

No files matched your search

@@ -1,28 +1,144 @@
# 04 — Проверяемая пользовательская установка Xray
**What to build:** Пользователь без root устанавливает официальный Xray одной
явной командой и получает проверенный бинарник нужной версии без скрытой замены
существующей установки или записи за пределами пользовательских каталогов.
**What to build:** Пользователь без root устанавливает официальный Xray
одной явной командой `install` и получает проверенный бинарник
зафиксированной версии, без скрытой замены существующей установки, без
записи за пределами пользовательских каталогов и без риска для живого
managed-экземпляра.
**Blocked by:** 01 — Внешний прокси и переносимый Bash-интерфейс.
**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:** ready-for-agent
**Status:** done
- [ ] `install` поддерживает Linux x86_64 и aarch64, по умолчанию выбирает
зафиксированную версию 26.3.27 и принимает другую только через явный параметр
версии, не объявляя произвольный релиз поддержанным.
- [ ] Архив и файл контрольной суммы скачиваются из официального релиза, после
чего SHA-256 проверяется до распаковки и установки; несовпадение прекращает
операцию без изменения установленного бинарника.
- [ ] Работа ведётся во временном пользовательском каталоге, извлечённый бинарник
сначала проходит проверку `version`, а затем атомарно устанавливается в
пользовательский каталог исполняемых файлов.
- [ ] Существующий бинарник другой версии не перезаписывается без `--replace`;
ошибка, отказ или прерывание не оставляют частично установленный файл.
- [ ] После установки `command -v xray` обязан указывать на установленный файл;
иначе команда завершается с понятной подсказкой исправить `PATH`.
- [ ] Установщик не использует sudo, systemd или системные каталоги, а команды
`start` и запуск без подкоманды никогда не начинают скачивание.
- [ ] Автоматические тесты проверяют обе архитектуры, корректную и повреждённую
контрольную сумму, существующую версию, `--replace`, атомарность и отсутствие
записи вне изолированного пользовательского каталога.
## Зафиксированный протокол установки
- **Только версия 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] `xray-proxy/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/headless-xray-proxy/token-ledger.md`.
@@ -39,6 +39,15 @@ _Заполняется по ходу; сравнивать построчно
| 03: перепроверка TASK-1..3 | Claude Opus (резюм субагента) | default | инкремент ~7.4k (суммарно 80 560) | 8 tool uses, 1.7 мин; FIXES_CONFIRMED |
| 03: перепроверка CODE-1..5 | Codex-ревьюер (resume) | medium | инкремент: свежих in ~112k, out ~3.2k | 20 команд; FIXES_CONFIRMED |
| 04: ревью постановки | Codex gpt-5.6-sol | high | in 2 469 937 (кэш 2 352 896 → свежих ~117k), out 17 051 (reasoning 10 128) | 54 команды; NEEDS_REWORK: 5 MAJOR + 3 MINOR + 1 NIT |
| 04: реализация | Codex gpt-5.6-sol | **medium** | in 2 941 682 (кэш 2 828 288 → свежих ~113k), out 18 521 (reasoning 5 278) | 50 команд; +~600 строк; 63 OK (+14) + 8 PASS |
| 04: саморевью | Codex (resume) | medium | инкремент: свежих in ~50k, out ~17.2k (reasoning ~9.2k) | 38 команд; found 6 fixed 6; 72 OK + 8 PASS |
| 04: слепое ревью «по задаче» | Claude Opus (субагент) | default | 53 463 суммарно | 18 tool uses, 5.7 мин; 3 NIT |
| 04: слепое ревью «по коду» | Codex gpt-5.6-sol (свежий тред) | high | in 1 774 005 (кэш 1 676 288 → свежих ~98k), out 10 402 (reasoning 5 481) | 46 команд; 2 MAJOR + 2 MINOR (CODE-1 — дефект постановки: реальный формат .dgst) |
| 04: триаж/починка | Codex (resume) | medium | инкремент: свежих in ~237k, out ~58.9k (reasoning ~24k) | FIXED 7, REJECTED 0; 79 OK (+30 к baseline) + 8 PASS |
| 04: перепроверка TASK-1..3 | Claude Opus (резюм субагента) | default | инкремент ~7.7k (суммарно 61 135) | 6 tool uses, 1.7 мин; FIXES_CONFIRMED |
| 04: перепроверка CODE-1..4 | Codex-ревьюер (resume) | medium | инкремент: свежих in ~207k, out ~12.5k (reasoning ~6.3k) | FIXES_CONFIRMED |
Итог 03 (реализация на MEDIUM): Codex — свежий input ~636k + output ~77k
(ревью постановки 82k/16k, реализация ~248k/29.5k, саморевью 53k/11.6k,
CODE-ревью 85k/12.8k, триаж 55.5k/16.9k, перепроверка 112k/3.2k);
@@ -49,6 +58,25 @@ Claude-доля — 80.5k суммарно (TASK-ревью + перепрове
\* usage первого (убитого) хода не был зафиксирован (нет `turn.completed`); цифры — из resume-хода, по масштабу покрывают весь тред.
Итог 04 (реализация на MEDIUM): Codex — свежий input ~822k + output ~135k
(ревью постановки 117k/17k, реализация 113k/18.5k, саморевью 50k/17.2k,
CODE-ревью 98k/10.4k, триаж 237k/58.9k, перепроверка 207k/12.5k);
Claude-доля — 61k суммарно (TASK-ревью + перепроверка). Слепые ревью
нашли 2 MAJOR + 2 MINOR + 3 NIT, причём CODE-1 (MAJOR) — дефект
постановки (spec-review на high выдумал однострочный `SHA256=`), т.е.
на счету реализации-medium фактически 1 MAJOR (границы размеров).
Примечательно: 04-medium вышла ДОРОЖЕ 06-high по свежим токенам
(~822k+135k против ~735k+97k) — экономию реализации съели дорогой триаж
(7 находок, включая переделку под исправленный протокол) и перепроверки.
Сводка эксперимента (свежий input + output Codex; MAJOR от слепых ревью):
| Задача | Effort реализации | Codex токены | Claude | MAJOR |
|---|---|---|---|---|
| 06 | high (baseline) | ~735k + 97k | 175k | 1 |
| 03 | medium | ~636k + 77k | 80.5k | 4 (гонки) |
| 04 | medium | ~822k + 135k | 61k | 2 (1 — вина постановки) |
## Наблюдения
- Кэш у Codex покрывает ~93% входа ревью-этапа: длинные треды с большим