refactor(proxy-client): переименован интерфейс прокси

- Зачем:
  - прежнее имя выпячивало Xray, хотя клиент поддерживает также внешний прокси, а два каталога конфигурации были неочевидны.
- Что:
  - пакет, CLI, конфигурация, состояние, тесты и документация переименованы в proxy-client.
  - машинный конфиг, профиль и два явных примера собраны в одном пользовательском каталоге.
  - свежая установка защищена от сворачивания каталога конфигурации внутрь репозитория.
- Проверка:
  - bash tests/test_proxy_client_full.sh.
  - bash tests/test_install.sh.
  - ручные start, status и xray run -test; незавершённая сетевая приёмка записана в handoff.
This commit is contained in:
ddadmin committed 2026-07-21 09:31:21 -04:00
1 parent e5323284a8
commit 6e431d875e
25 files changed
+310 -227

No files matched your search

@@ -0,0 +1,70 @@
# 01 — Внешний прокси и переносимый Bash-интерфейс
**What to build:** Самостоятельный пользовательский пакет прокси, через который
можно безопасно запускать команды с уже работающим внешним прокси, например
Hiddify. Привычные Bash-команды переходят на единый CLI-контракт, но прежний
сценарий без машинного конфига продолжает работать.
**Blocked by:** None — can start immediately.
**Status:** done
Часть требований пересмотрена в 06 — см. 06-simplification-pass.md.
- [x] CLI работает на Python 3.8+ без сторонних библиотек, ничего не устанавливает
и не запускает без явной подкоманды и выдаёт понятную ошибку на более старом
Python.
- [x] Машинный INI явно различает типы `external` и `xray`, проверяет имя, адрес,
порт и добавку к `NO_PROXY`; отсутствие файла сохраняет внешний прокси
`hproxy.local:12334` без неявного переключения на другой тип.
- [x] Канонический машинный INI содержит единственную секцию `[proxy]` и ровно
по одному ключу `type`, `name`, `address`, `port`, `no_proxy_extra`:
```ini
[proxy]
type = external
name = Hiddify
address = hproxy.local
port = 12334
no_proxy_extra = .example.internal,git.example.internal
```
Все ключи обязательны при наличии файла; `no_proxy_extra` может быть пустым.
Неизвестные секции и ключи, повторы, пропуски, многострочные или содержащие
NUL значения, пустые имя или адрес и порт вне диапазона 1–65535 отклоняются.
Добавки к `NO_PROXY` задаются через запятую, окружающие пробелы удаляются,
пустые элементы отклоняются.
- [x] `address` принимает IPv4, IPv6 без квадратных скобок либо ASCII DNS-имя
из корректных непустых меток; схема URL, встроенный порт, скобки, zone ID,
пробелы, `/`, `@` и другие URL-разделители отклоняются. При формировании
proxy-URL CLI сам заключает IPv6-литерал в квадратные скобки.
- [x] Для типа `external` команды `status`, `env` и `run -- COMMAND ARG...`
проверяют только настроенный адрес и порт; недоступный прокси запрещает запуск
дочерней команды с понятным сообщением.
- [x] До реализации управляемого процесса конфиг с `type=xray` обрабатывается
безопасным отказом: `status`, `env`, `run` и `start` не считают один открытый
порт доказательством управляемого Xray и не запускают дочернюю команду.
- [x] `env` при успехе печатает только восемь разрешённых proxy-переменных в двух
регистрах, а при ошибке не печатает частичный результат; базовый `NO_PROXY`
включает loopback и узел прокси, затем дополняется машинной настройкой.
- [x] Документированный `PROXY_PREFLIGHT_TIMEOUT` сохраняется как положительное
число секунд с прежним значением по умолчанию `2` для проверок доступности из
`status`, `env` и `run`; ошибочное значение даёт понятный отказ.
- [x] `run` без искажений сохраняет аргументы и код возврата команды, передаёт
proxy-переменные только дочернему процессу и возвращает отдельную понятную
ошибку, если команда отсутствует в `PATH`.
- [x] Переносимый Bash-модуль становится единственным владельцем `proxy_on`,
`proxy_off`, `pclaude`, `pcodex`, `myip` и внутренних proxy-помощников;
WSL-функция обновления `hproxy.local` остаётся независимой.
- [x] Обычный запуск dotfiles подключает общий Bash-файл и переносимый
proxy-модуль независимо: после установки доступны все пять proxy-команд и
`update_hproxy`; подключение одного переносимого модуля без остальных
dotfiles также предоставляет все пять proxy-команд.
- [x] Bash-модуль принимает вывод `env` атомарно, проверяет белый список и
наличие каждого из восьми ключей ровно один раз, не использует `eval` и при
любом неверном выводе оставляет прежние восемь переменных без изменений;
`proxy_on` и `proxy_off` больше не делают автоматический запрос внешнего IP.
- [x] Автоматические тесты подтверждают прежний внешний Hiddify-сценарий,
fail-fast, оба регистра переменных, настраиваемый `NO_PROXY`, отсутствие
протечки окружения, обычное и самостоятельное подключение Bash-модуля,
безопасный отказ для `type=xray` и атомарный разбор полного вывода `env`.
@@ -0,0 +1,74 @@
# 02 — Управляемый Xray: полный штатный путь
**What to build:** Пользователь с уже установленным поддерживаемым Xray кладёт
одну VLESS-ссылку в закрытый файл и получает полный штатный сценарий локального
прокси: проверку профиля, запуск на переднем плане, диагностику, запуск команд
через прокси и штатную остановку.
**Blocked by:** 01 — Внешний прокси и переносимый Bash-интерфейс.
**Status:** done
Часть требований пересмотрена в 06 — см. 06-simplification-pass.md.
- [x] `start` разрешён только при явном типе `xray`, требует loopback-адрес и
согласованный локальный порт, никогда не скачивает бинарник и выбирает первый
`xray` из `PATH`. Один разрешённый путь используется для `version`,
`run -test` и основного процесса и показывается перед запуском.
- [x] Успешный `xray version` разбирается только по первой строке в форме
`Xray VERSION` с концом строки либо ASCII-пробелом после полного токена
версии. Принимаются только `25.12.8` и `26.3.27`; ведущий мусор, похожий
префикс вроде `26.3.270`, пустой, неверный или завершившийся ошибкой вывод
запрещает преобразование и запуск. Запрошенная или указанная в профиле версия
не подменяет фактическую версию выбранного бинарника.
- [x] Профиль берётся из предусмотренного файла по умолчанию либо из явно
переданного `-f`, содержит ровно одну VLESS-ссылку, а конечный файл после
разрешения симлинка недоступен группе и остальным пользователям.
- [x] Строгий преобразователь поддерживает TCP/RAW с REALITY и XHTTP с TLS,
сохраняет все предусмотренные спецификацией поля, отклоняет повторы,
любой неизвестный query-параметр, отсутствующие обязательные поля и
неподдерживаемые сочетания до запуска Xray.
- [x] Для Xray 26.3.27 корректно обрабатываются `pcs`/`vcn`, а устаревшие
`allowInsecure`/`insecure` не переносятся и дают предупреждение; для явно
выбранного Xray 25.12.8 сохраняется совместимое insecure-поведение, а новые
поля отклоняются без автоматического отката версии.
- [x] `allowInsecure` и `insecure` принимают только точные значения `true` или
`false`; при одновременном присутствии значения должны совпадать. Для
25.12.8 только согласованное истинное значение включает `allowInsecure` с
предупреждением, а для 26.3.27 поле никогда не попадает в JSON и истинное
значение предупреждает об обычной проверке сертификата.
- [x] Созданный клиентский JSON слушает только loopback, предоставляет HTTP и
SOCKS на одном порту, отключает журнал доступа и проходит `xray run -test`
до запуска основного процесса.
- [x] `start` остаётся на переднем плане как супервизор одного дочернего Xray;
после запуска процесс жив и порт принимает соединения, а сбой отдельной
проверки внешнего IP остаётся предупреждением.
- [x] Штатный `start` создаёт закрытое состояние с PID супервизора и дочернего
Xray, разрешённым путём к бинарнику, портом и безопасным отпечатком профиля.
`status`, `env`, `run` и `stop` требуют это состояние, живой супервизор,
ожидаемый дочерний процесс и настроенный порт; один чужой listener или PID
никогда не считается управляемым Xray и не затрагивается.
- [x] `status`, `env`, `run` и `stop` обеспечивают штатный путь управляемого
Xray; `run` никогда не запускает его скрыто, а `stop` может завершить
управляемый процесс после переключения машинного типа на `external`.
Успешный `stop` посылает сигнал супервизору, ограниченно ждёт завершения
супервизора и Xray, очищает штатное состояние и возвращает исходный
foreground-вызов `start` в терминал.
- [x] Диагностика внешнего IP действительно делает запрос через настроенный
локальный HTTP-прокси. Её сбой явно показан как предупреждение и не отменяет
уже успешные проверки процесса и порта.
- [x] Безопасная сводка показывает только полезные несекретные свойства
профиля; XHTTP `path` показывается только как скрытое значение с длиной, а
UUID, ключи, short ID и другие секреты — без фрагментов исходного значения.
Полная VLESS-ссылка и уникальные секреты отсутствуют в stdout, stderr и argv
дочерних процессов при успехе, любом отказе разбора и сбое `run -test`.
- [x] Модульные и интеграционные тесты покрывают обе семьи ссылок, варианты
ALPN, типичную ссылку Hiddify XHTTP/TLS, обе поддержанные версии Xray и
понятные отказы на неподдерживаемом вводе.
- [x] Чёрный ящик для каждой поддержанной версии доказывает полный порядок:
профиль по умолчанию и `-f`, `version`, успешный `run -test` до основного
запуска, живое управляемое состояние и порт, `status`, `env`, дочернюю
команду через `run`, проксированную IP-диагностику, `stop` и ограниченное
завершение исходного `start`. Через один настроенный локальный порт реально
завершаются отдельный HTTP-proxy-запрос и отдельный SOCKS-proxy-запрос.
Подмена порта или состояния чужим процессом даёт безопасный отказ.
@@ -0,0 +1,132 @@
# 03 — Безопасный жизненный цикл управляемого процесса
**What to build:** Управляемый Xray предсказуемо переживает повторные команды,
сигналы, устаревшее состояние и аварийное исчезновение супервизора, не
затрагивая чужие процессы и не оставляя секретных данных вне закрытого
служебного каталога.
**Blocked by:** 02 — Управляемый Xray: полный штатный путь; 06 — Упрощение.
**Status:** done
**Резолюция (2026-07-20):** реализовано и принято. Дельта: CLI 976→1235,
тесты 437→521 + 799→1112; сьюты: 49 unittest OK + 8 shell PASS (baseline
34/8). Конвейер: spec-ревью постановки (NEEDS_REWORK, 7 MAJOR + 3 MINOR —
постановка переписана до реализации) → реализация на medium → саморевью
(3 находки, починены) → слепые ревью: TASK 1 MINOR + 2 NIT, CODE 4 MAJOR +
1 MINOR (гонки идентичности/очистки) → триаж: FIXED 8, REJECTED 0 → обе
перепроверки FIXES_CONFIRMED.
## Модель идентичности процессов
Идентичность определяется отдельно для ребёнка-Xray и для супервизора;
«обход argv в поисках любого совпадения» (`verify_managed_state`) заменяется
этим механизмом.
- **Общее для обоих:** PID + время старта. Время старта — поле 22
(`starttime`) из `/proc/PID/stat`, целое число тиков с момента загрузки;
хранится в state как есть и сравнивается на точное равенство. Парсер
обязан сначала изолировать поле `comm` по последней `)` (comm может
содержать пробелы и скобки) и только потом индексировать поля; любая
ошибка чтения/разбора трактуется как несовпадение (fail-closed).
- **Ребёнок Xray:** канонический путь `/proc/PID/exe` (realpath) совпадает
с сохранённым `xray_path`.
- **Супервизор:** его exe — интерпретатор Python, поэтому проверяется
иначе: в state сохраняется канонический путь скрипта CLI
(`supervisor_path`, realpath на момент старта); при проверке операнд
скрипта из `/proc/PID/cmdline` резолвится (относительные пути — через
`/proc/PID/cwd`) и сравнивается с `supervisor_path`. Поддерживаемые формы
запуска (относительный путь, PATH, симлинк) не ломаются — сравнение
идёт по realpath.
- **Границы загрузки:** в state сохраняется `boot_id`
(`/proc/sys/kernel/random/boot_id`). Несовпадение с текущим означает
устаревшее состояние: записанные PID трактуются как мёртвые, сигналы не
посылаются.
- **Гонка check-then-kill:** непосредственно перед каждым `os.kill`
идентичность перечитывается заново; несовпадение — отказ без сигнала.
Остаточная гонка «процесс умер между перечиткой и kill» принята как риск
модели доверенных пользователей (pidfd не используем: Python 3.8+,
переносимость).
## Чекбоксы
- [x] Взаимное исключение: супервизор на всё время жизни держит
неблокирующий эксклюзивный `fcntl.flock` на lock-файле в служебном
каталоге. Второй одновременный `start` завершается понятным отказом, не
запуская процессов и не трогая чужие state/конфиг. `state.json` остаётся
данными для восстановления после сбоя, а не механизмом блокировки.
Последовательные повторный `start` и смена профиля при живом экземпляре
по-прежнему отвергаются.
- [x] Схема state (права 0600, каталоги 0700) — ровно эти ключи:
`supervisor_pid`, `xray_pid`, `xray_path`, `config_path`, `address`,
`port` (существующие) + `supervisor_path`, `supervisor_starttime`,
`xray_starttime`, `boot_id` (новые). Ссылка VLESS и её секреты в state
не попадают.
- [x] `status` и `stop` применяют модель идентичности выше; чужой,
устаревший или переиспользованный PID не признаётся управляемым
процессом (в пределах принятой остаточной гонки).
- [x] Три исхода проверки состояния, у каждого — своё поведение:
1. **Здоровая пара** (супервизор и ребёнок живы, идентичность
подтверждена): `stop` сигналит супервизору, как сейчас.
2. **Сирота** (супервизор исчез, ребёнок жив и подтверждён по
PID + starttime + boot_id + exe): `stop` сигналит ребёнку напрямую и
удаляет state и сгенерированный конфиг; доступность порта для этого
НЕ требуется. `status` — отдельная ненулевая диагностика «сирота»;
`env` и `run` — отказ.
3. **Непроверяемое/устаревшее состояние:** если записанный PID жив, но
идентичность не подтверждена — никаких сигналов, state сохраняется,
диагностика с инструкцией ручного удаления; если записанные PID
мертвы (или boot_id не совпал) — безопасная очистка устаревших
файлов разрешена.
- [x] Конфликт локального порта: `start` при уже занятом порте завершается
ненулевым кодом, диагностика называет порт занятым/недоступным, чужой
слушатель не затронут, свои сгенерированные файлы убраны. Инспекция
владения слушателем (/proc/net/tcp) запрещена.
- [x] Секреты: полная VLESS-ссылка никуда не персистится; UUID, ключи,
секретоподобные значения отсутствуют в stdout, stderr, argv,
`state.json` и lock-файле. Единственное допустимое место — `client.json`
с правами 0600 внутри каталога 0700; он удаляется при штатном завершении
и при `stop` сироты.
- [x] Автоматические тесты (без root, на обычной dev-машине):
- блокировка: удержанный извне flock → `start` отказывает, ничего не
запустив и не изменив;
- переиспользованный PID моделируется правкой сохранённого `starttime`
у живого процесса (реальную гонку PID не воспроизводим); отдельно —
мёртвый PID, неверный исполняемый файл, несовпавший `boot_id`;
- SIGKILL супервизора → `stop` добивает подтверждённого сироту и чистит
state/конфиг;
- занятый порт (заранее открытый сокет) → контракт из чекбокса выше;
- юнит-тест парсера `/proc/PID/stat` на синтетических `comm` с
пробелами и `)`;
- фикстура фейкового Xray адаптирована под проверку `/proc/PID/exe`
(ребёнок должен быть настоящим исполняемым файлом — например,
`sys.executable` как «бинарник xray» со скриптами-операндами; детали
на усмотрение исполнителя);
- все сохраняемые ниже поведения остаются зелёными.
## Сохранить, не переписывать
Уже реализовано и покрыто тестами в 02/06 — не переделывать, только не
сломать: проброс SIGINT/SIGTERM/SIGHUP ребёнку; очистка state и конфига при
штатном завершении; права 0600/0700; последовательные отказы (повторный
start, смена профиля, несовпадение машинного адреса/порта со state);
loopback-only; секреты вне stdout/stderr/argv; foreground-супервизор;
fail-fast `pclaude`/`pcodex`; внешний путь без машинного конфига.
Запрещено восстанавливать срезанное в 06: владение слушателем через
`/proc/net/tcp`, `profile_fingerprint`, инспекцию zombie-состояний,
SIGALRM-машинерию, поддержку второй версии Xray.
## Не трогать
- `bash/.bash_functions`, установщик, README — вне объёма.
- Никаких коммитов.
## Отчёт исполнителя
1. Изменённые файлы со строкой «зачем» на каждый.
2. Команды тестов и счётчики (`python3 -m unittest discover -s tests`,
`bash tests/test_proxy_launchers.sh`; baseline: 34 OK / 8 PASS — новых
станет больше, указать числа).
3. Чекбоксы: закрыт / не закрыт и почему.
4. Принятые по ходу решения и сомнения, если есть.
@@ -0,0 +1,144 @@
# 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`.
@@ -0,0 +1,118 @@
# 05 — Доставка пакета и итоговая приёмка
**What to build:** Поставка `proxy-client` проверена целиком: корневой обзор
соответствует готовым возможностям, автоматические проверки воспроизводимы, а
ручная приёмка на доступных системах подготовлена для пользователя.
**Blocked by:** 03 — безопасный жизненный цикл; 04 — проверяемая установка.
**Status:** ready-for-human
## Границы задачи
Тикеты 03/04/06 уже доставили CLI, Bash-модуль, установку и жизненный цикл.
Задача 05 их проверяет, но не переписывает. Источники истины:
- [`proxy-client/README.md`](../../../proxy-client/README.md) — пользовательские
команды, настройка и ограничения;
- [исходный дизайн](../../../docs/specs/2026-07-15-proxy-client-design.md)
— границы и принятые решения.
- [x] Корневой `README.md` кратко описывает внешний Hiddify и управляемый Xray,
показывает `stow proxy-client` и ссылается на полную инструкцию.
- [x] Пакет подключается через stow, ручные ссылки или копирование; общий
установщик использует тот же пакет.
- [x] Инструкция покрывает две семьи VLESS, Xray 26.3.27, отсутствие systemd,
автозапуска и автоматического перехода между прокси.
- [x] Тест
`ManagedXrayCliTest.test_exact_boolean_version_alpn_and_extra_branches`
подтверждает, что устаревшие `allowInsecure`/`insecure` не попадают в
TLS-конфиг, а истинное значение даёт предупреждение.
## Секретные данные
- [x] В отслеживаемых файлах, тестовых данных, аргументах, выводе и отчёте нет
настоящих VLESS-ссылок, UUID, ключей, `shortId` и `path`. Автоматические тесты
используют только вымышленные контрольные значения. Настоящий профиль и
сгенерированный JSON остаются в закрытых пользовательских файлах и не
копируются в отчёт.
## Автоматическая проверка
- [x] `tests/test_proxy_client_full.sh` запускает весь Python-набор под системным
`python3` и CPython 3.8 через `uv`, затем Bash-тесты и проверку установки.
- [x] Тестовый Xray остаётся отдельным исполняемым файлом и получает доступ к
библиотекам исходного Python. Это устраняет ложные падения переносимой сборки
`uv`, не ослабляя проверки личности процесса.
- [x] `tests/test_install.sh` собирает небольшие тестовые деревья только из
отслеживаемых Git-файлов и удаляет общий временный каталог при выходе.
## Приёмка пользователем
Исполнитель готовит команды и останавливается. Результат отмечает пользователь;
недоступная система остаётся неподтверждённой.
- [ ] Ubuntu с Python 3.10: установка, жизненный цикл, `xray run -test`, HTTP и
SOCKS через один локальный вход Xray 26.3.27.
- [ ] РЕД ОС, Astra Linux или Alt Linux по доступности: тот же сценарий после
проверки фактической версии Python.
## После приёмки
- [ ] Статус исходного дизайна и заявления о поддержке приведены в соответствие
только с фактически выполненными прогонами.
## Comments
### 2026-07-21 — автоматическая проверка
`bash tests/test_proxy_client_full.sh` завершился с кодом 0:
- системный Python 3.14: 79 тестов, `OK`, 61,034 с;
- Bash-интеграция: 8 проверок, `PASS`; `shellcheck` отсутствует и явно пропущен;
- установка: `All install tests passed`; в `/tmp` не осталось каталогов
`dotfiles-install-test.*`;
- CPython 3.8 через `uv`: 79 тестов, `OK`, 52,446 с.
### 2026-07-21 — команды ручной приёмки
В первой панели tmux, после подготовки закрытого `default.url` и машинного
конфига с `type = xray`:
```bash
python3 --version
cd ~/dotfiles
stow proxy-client
source ~/.config/proxy-client/shell.bash
proxy-client install
proxy-client start
```
Во второй панели, пока `start` работает:
```bash
proxy-client status
xray run -test -c "${XDG_STATE_HOME:-$HOME/.local/state}/proxy-client/client.json"
curl --proxy http://127.0.0.1:12334 https://api.ipify.org
curl --proxy socks5h://127.0.0.1:12334 https://api.ipify.org
proxy-client stop
```
Если настроен другой локальный адрес или порт, те же значения подставляются в
обе команды `curl`. В отчёт записываются только версии, коды возврата и итог
проверки HTTP/SOCKS.
### 2026-07-21 — переименование и частичная ручная приёмка
Публичный интерфейс переименован в `proxy-client`, а машинный конфиг и закрытый
профиль объединены в `~/.config/proxy-client/`. В ходе разработки текущий HOME
приведён к новой структуре; старые пользовательские пути отсутствуют, права
файлов `600` сохранены. Поддержка прежнего имени в установщик не добавлялась,
поскольку выпуск ещё не состоялся.
Xray 26.3.27 установился и запустился, `status` подтвердил процесс на
`127.0.0.1:12334`, а `xray run -test` завершился сообщением
`Configuration OK`. HTTP и SOCKS5h через локальный вход не прошли: оба запроса
`curl` завершились кодом 35 с неожиданным закрытием TLS-соединения во время
рукопожатия. После проверки Xray штатно остановлен. Пункты ручной приёмки пока
не отмечены.
@@ -0,0 +1,180 @@
# 06 — Упрощение: срез accidental-сложности спеки и кода
**What to build:** Один проход упрощения по готовым issue 01–02 и спеке:
убрать поддержку второй версии Xray, избыточные защитные проверки вне
принятой модели угроз («доверенные пользователи машины», единственный
пользователь редактирует свои файлы сам) и церемонии, не нужные личному
foreground-инструменту. Поведенческие контракты из раздела «Не трогать»
сохраняются без изменений.
**Blocked by:** 02 — Управляемый Xray: полный штатный путь.
**Status:** done
**Резолюция (2026-07-20):** реализовано и принято. Дельта: CLI 1123→976,
shell.bash 116→91, тесты 1357→1236 строк; сьюты: 34 unittest OK + 8 shell
PASS. Ревью: 1 MAJOR (scoped IPv6 в упрощённом валидаторе — починен с
регрессионным тестом под Python 3.8), 1 NIT отклонён аргументированно.
Non-gating замечания ревью приняты как риски модели угроз.
**Сначала прочитать:**
1. Этот файл целиком, затем `/tmp/xray-simplify-Zeo5/06-simplify/sweep.md` —
там точный line-level blast radius каждого пункта (секция 2 — срезы A/B,
секция 3 — SIMP-N). Номера строк — ориентир, сверяй с фактическим кодом.
2. `docs/specs/2026-07-15-proxy-client-design.md` — исходная спека.
3. `proxy-client/.local/bin/proxy-client`, `proxy-client/.config/proxy-client/shell.bash`,
`tests/test_proxy_client.py`, `tests/test_proxy_client_managed.py`.
Команды проверки (обе обязательны):
- `python3 -m unittest discover -s tests` — baseline до изменений: 38
тестов, OK, ~60 с;
- `bash tests/test_proxy_launchers.sh` — baseline: 9 проверок PASS.
После изменений тестов станет меньше (удаляются тесты срезанных фич); все
оставшиеся зелёные, новые счётчики указать в отчёте.
## A. Срез dual-version: остаётся только Xray 26.3.27
- [x] `VERSION_LINE` принимает только `26.3.27`; параметр `version` убран из
сигнатур `parse_vless`, `build_xray_config`, `safe_summary`, `warn_legacy`
и их вызовов; `xray_version` остаётся воротами «поддержанная версия или
отказ» и по-прежнему показывает фактическую версию в сводке.
- [x] Ветка эмиссии `allowInsecure` для 25.12.8 и запрет `pcs`/`vcn` для
25.12.8 удалены. `parse_boolean_fields` (валидация точных `true`/`false`,
требование согласованности обоих полей, предупреждение при истинном
значении) сохраняется; `allowInsecure` никогда не попадает в JSON.
- [x] Тестовая ось версий схлопнута: lifecycle-тест гоняет один прогон
26.3.27 (обе локации профиля — default и `-f` — сохраняются);
`test_old_version_preserves_explicit_legacy_insecure` и все
25.12.8-строки табличных тестов удалены.
## B. Срез проверки владения сокетом через /proc/net/tcp
- [x] `process_owns_listener`, `process_socket_inodes`, `decode_proc_address`
и их вызовы (в `verify_managed_state` и readiness-цикле `command_start`)
удалены. Сохраняются: сверка PID/идентичности процесса, сверка
адрес/порт машинного конфига со state и `check_endpoint`-проверка
доступности порта.
- [x] `test_verified_child_must_own_configured_listener` удалён вместе с
веткой `FAKE_XRAY_LISTEN_PORT` в фейковом xray;
`test_substituted_machine_port_fails_closed...` сохраняется и зелёный.
- [x] Диагностические сообщения, упоминающие проверку владения слушателем
(в `verify_managed_state` и readiness-цикле `command_start`), обновлены:
после среза они описывают фактические проверки (идентичность процесса,
сверка адрес/порт, доступность порта), а не владение сокетом.
## C. Диета входной валидации (файлы пишет сам пользователь)
- [x] `validate_address`: сначала `ipaddress.ip_address`, иначе минимальная
проверка «непустое ASCII-имя без пробелов, схемы `://`, `@`, `/` и
квадратных скобок». RFC-строгость по меткам DNS, лимит 253 и
сопутствующие тесты удалены. Оборачивание IPv6 в `[]` при сборке URL
сохраняется.
- [x] `load_config`: сохраняется отказ на неизвестной секции/ключе и на
отсутствии обязательного ключа (ловит реальные опечатки) и диапазон
порта. Удалены: скан NUL, guard UTF-8-декодирования, отдельный запрет
`[DEFAULT]`, запрет CR/многострочных значений, case-sensitive
`optionxform`; соответствующие подкейсы тестов удалены.
- [x] `read_profile`: сохраняются разрешение симлинка, проверка прав
(group/other → ошибка с подсказкой `chmod 600`) и правило «ровно одна
непустая строка». Удалены скан NUL и guard UTF-8.
- [x] `parse_query`: сохраняются отказ на неизвестном и на повторном
query-параметре. Удалены regex на сырые `%`-последовательности и
посимвольный запрет управляющих символов.
## D. Диета церемоний
- [x] `write_private_json`: простая запись файла с правами 0600 внутри
уже-0700 каталога; O_EXCL/fsync/`os.replace`/PID-суффиксы удалены.
- [x] Валидация `PROXY_PREFLIGHT_TIMEOUT` и `PROXY_CLIENT_STOP_TIMEOUT` — один
guard «`float()` успешен и > 0, иначе понятная ошибка»; `math` не
импортируется; тесты экстремальных значений (nan/inf/1e-7/1e308)
удалены. Для `PROXY_PREFLIGHT_TIMEOUT` оставить один тест «плохое
значение → понятный отказ»; для `PROXY_CLIENT_STOP_TIMEOUT` отдельного
теста нет и добавлять его не нужно — достаточно guard'а.
- [x] `check_reachable` использует `socket.create_connection(timeout=...)`
без SIGALRM/setitimer-машинерии. Принятый риск (записан здесь):
зависший DNS-резолв на external-пути не ограничен таймаутом — для
личного инструмента пользователь прерывает вручную.
- [x] `extra` и `alpn` разбираются один раз в `parse_vless`, результат
сохраняется в профиле и переиспользуется в `build_xray_config` без
повторного разбора. `parse_json_object` сохраняет оба отказа —
на повторах ключей и на JSON-константах (`reject_constant` остаётся):
существующий тест требует отказ на уровне разбора с диагностикой про
`extra` и без запуска `run -test`, и это дешевле, чем переносить
ответственность на сериализацию.
- [x] `process_alive` — только `os.kill(pid, 0)`; чтение
`/proc/PID/stat` ради zombie-состояния удалено.
- [x] Отпечаток профиля (`profile_fingerprint`) удалён из state и кода:
он не участвует ни в одном решении. Смена профиля по-прежнему
блокируется существованием state. Вместе с ним обновлены `STATE_KEYS` и
все тестовые фикстуры state (включая точную сверку набора ключей в
managed-тестах): устаревший ключ в фикстуре не должен ронять сверку
ключей раньше, чем тест дойдёт до проверяемой идентичности процесса.
## E. shell.bash: средняя форма приёма `env`
- [x] `_proxy_apply_env_file`: при ненулевом коде возврата CLI окружение не
меняется вовсе; успешный вывод разбирается построчно по первому `=`;
применяются (через `printf -v`, без `eval`) только восемь известных
ключей, прочие строки — ошибка без частичного экспорта. Удалены сканы
NUL/CR, отдельный детектор повторов и проверка «ровно восемь строк».
Свойство «нет частичного изменения окружения при ошибке» сохраняется и
остаётся покрыто тестом.
- [x] `tests/test_proxy_launchers.sh` приведён к средней форме: проверки
повторов/CR/NUL/счётчика строк, соответствующие удалённой машинерии,
убраны или переписаны; проверки «ненулевой код возврата CLI → окружение
не тронуто», «неизвестная строка → ошибка без частичного экспорта» и
владения функциями остаются зелёными.
## F. Спека и соседние issue приведены к новому объёму
- [x] Из спеки удалены: ветка 25.12.8 (все места из секции 2 sweep-файла),
флаги `-v`/`--debug` (не реализованы), упоминание отпечатка профиля в
state; описания валидации/атомарности смягчены до фактического
поведения после этого issue. Формулировка идентичности процессов
(«PID + время старта + исполняемый файл») в спеке не меняется.
- [x] Issue 03: добавлен явный мандат «идентичность = PID + время старта
(`/proc/PID/stat`) + исполняемый файл ребёнка / путь скрипта
супервизора; текущий argv-обход заменить этим механизмом»; убраны
унаследованные упоминания срезанных фич, если есть; строка Blocked by
дополнена: «06 — Упрощение».
- [x] Issue 05: приёмка сведена к одной версии Xray 26.3.27; упоминания
25.12.8 удалены. Пункты про РЕД ОС сохранены (решение пользователя
2026-07-20: целевые системы шире Ubuntu — РЕД ОС, возможно Astra Linux
и Alt Linux; инструмент могут использовать коллеги).
- [x] В спеке в раздел ручной проверки добавлены Astra Linux и Alt Linux
как кандидаты «по мере доступности» с тем же правилом, что для РЕД ОС:
недоступная среда явно записывается как неподтверждённая, поддержка не
заявляется без фактической проверки версии Python и прогона.
- [x] В issue 01 и 02 добавлена одна строка после Status: «Часть
требований пересмотрена в 06 — см. 06-simplification-pass.md»; чекбоксы
01 и 02 не редактируются (issue 01 описывает строгую приёмку `env`,
которую этот issue смягчает, — без пометки он противоречил бы коду).
## G. Не трогать (границы)
- Строгий отказ на неизвестном/повторном query-параметре VLESS.
- Точечный `stop` только по проверенному PID; никакого pkill.
- Foreground-супервизор, проброс сигналов, очистка state.
- Изоляция окружения `run` (proxy-переменные только у ребёнка), сохранение
argv и кода возврата, `--`-разделитель.
- Fail-fast `pclaude`/`pcodex`, обратная совместимость: без машинного
конфига — внешний `hproxy.local:12334`.
- Проверка прав файла профиля (chmod 600) и loopback-only вход.
- Авто-диагностика внешнего IP на старте (несмертельная) — остаётся.
- Секреты: полная VLESS-ссылка, UUID, ключи не появляются в stdout/stderr/
argv — все существующие тесты на это остаются зелёными.
- `bash/.bash_functions`, установщик, README — не входят в этот issue.
- Никаких коммитов.
## Отчёт исполнителя
1. Изменённые файлы со строкой «зачем» на каждый.
2. Команда тестов и счётчик (все зелёные, указать новое число).
3. Чекбоксы: закрыт / не закрыт и почему.
4. Итоговая дельта `wc -l` по CLI, shell.bash и обоим тест-файлам
против baseline (1123 / 116 / 467 / 890).
5. Сомнения, если есть.