diff --git a/docs/superpowers/specs/2026-05-19-proxy-cli-launchers-design.md b/docs/superpowers/specs/2026-05-19-proxy-cli-launchers-design.md new file mode 100644 index 0000000..606e7a3 --- /dev/null +++ b/docs/superpowers/specs/2026-05-19-proxy-cli-launchers-design.md @@ -0,0 +1,171 @@ +# Дизайн: `pclaude` / `pcodex` — явный запуск claude/codex CLI через прокси + +Дата: 2026-05-19 +Статус: утверждён (дизайн), ожидает плана реализации + +## Проблема + +`claude` и `codex` CLI должны ходить в сеть через прокси +(`hproxy.local:12334`). Сейчас это обеспечивается ручным `proxy_on` в текущем +shell — легко забыть, и тогда трафик уходит мимо прокси. + +## Цель и подход + +Цель — **удобство и best-effort**, а не сетевая гарантия: дать одну явную +команду, после которой не нужно помнить про `proxy_on` и которая не стартует +молча, если прокси недоступен. Это **не** fail-closed изоляция — голые +`claude`/`codex` по-прежнему могут уйти мимо прокси (см. «Известные +ограничения»). + +**Проверенное допущение:** `claude` и `codex` уважают +`HTTP(S)_PROXY`/`ALL_PROXY` — подтверждено эмпирически (ежедневная работа в +этом режиме). На этом стоит весь env-подход; spike не требуется. + +Ввести **отдельные явные команды** `pclaude` и `pcodex`. Бинарники +`claude`/`codex` не переименовываются и не перекрываются — явное `p`-имя +выражает намерение и исключает риск «где-то нет алиаса → запуск мимо прокси». + +Подход — **convenience-обёртка** (не сетевая изоляция): + +1. выставить proxy-env только для дочернего процесса (не загрязняя + родительский shell/сессию); +2. **fail-fast pre-flight**: перед запуском быстро проверить, что + `hproxy.local:12334` доступен; если нет — **не запускать**, выдать + понятную ошибку и ненулевой код возврата. Семантика проверки — + *reachability* (порт прокси открыт), а не доказательство, что трафик + реально проксируется; последнее покрывается проверенным допущением выше. + Таймаут вынесен в переменную (`PROXY_PREFLIGHT_TIMEOUT`, дефолт 2 c). + +Реализация на двух платформах: + +- Linux/WSL bash; +- Windows native PowerShell. + +Эндпоинт прокси один и тот же на обеих платформах (`hproxy.local:12334`) и +переиспользуется из уже существующих `proxy_on`. + +### Не входит в объём (YAGNI) + +- Сетевая изоляция / firewall / network namespace (fail-closed). +- Перекрытие/переименование оригинальных `claude`/`codex`. +- Поддержка не-PowerShell оболочек на Windows (cmd.exe и т.п.). +- Изменение существующего поведения `proxy_on`/`proxy_off`/`myip`. + +## Ключевой принцип: единый источник правды + +Эндпоинт прокси сейчас захардкожен в `proxy_on` дважды (bash и PS-профиль). +Чтобы `pclaude`/`pcodex` не размножили константы ещё в нескольких местах, +значения выносятся в небольшой внутренний хелпер в каждом файле, и +существующий `proxy_on` рефакторится на этот же хелпер. Это единственный +сопутствующий рефактор, ограниченный задачей. + +## Компоненты — Linux + +Файл: `bash/.bash_functions` (stow → `~/.bash_functions`, уже подключается +через `~/.bash_custom`). Новых установочных шагов нет. + +1. `_proxy_export()` — внутренний. Экспортирует `HTTP_PROXY`, `HTTPS_PROXY`, + `ALL_PROXY`, `NO_PROXY` (+ lower-case дубли) из единого набора локальных + переменных. `proxy_on()` рефакторится так, чтобы вызывать `_proxy_export`; + его эхо и проверка внешнего IP остаются без изменений. +2. `_proxy_reachable()` — TCP-connect к `hproxy.local:12334` с таймаутом + `${PROXY_PREFLIGHT_TIMEOUT:-2}` c + (`timeout "$t" bash -c '... >/dev/tcp/hproxy.local/12334'`). Возвращает + 0/1, ничего не печатает. Таймаут покрывает и медленный DNS-резолв + `hproxy.local` (при зависании резолва — false-negative и отказ запуска, + что является безопасным направлением отказа). +3. `_proxy_run [args…]`: + - `_proxy_reachable` → при неудаче печатает понятное сообщение + (рус., стиль существующих `❌`/`✅`), `return 1`, **бинарник не + запускается**; + - проверка наличия `` в PATH (`command -v`) → при отсутствии + отдельное понятное сообщение, ненулевой код; + - запуск в subshell: `( _proxy_export; exec command "" "$@" )` — + proxy-env виден только дочернему процессу, не протекает в родительский + shell; код возврата пробрасывается. +4. `pclaude() { _proxy_run claude "$@"; }` + `pcodex() { _proxy_run codex "$@"; }` + +## Компоненты — Windows + +Файл: `chezmoi/dot_config/powershell/profile.ps1` (уже доставляется chezmoi и +подключается через `Microsoft.PowerShell_profile.ps1.tmpl`). Новых +установочных шагов нет. + +1. `Set-ProxyEnv` — внутренний. Ставит `HTTP_PROXY`, `HTTPS_PROXY`, + `ALL_PROXY`, `NO_PROXY` из единого источника. `proxy_on` рефакторится на + него (его вывод/`Get-PublicIp` не меняются). +2. `Test-ProxyReachable` — `System.Net.Sockets.TcpClient` к + `hproxy.local:12334` с таймаутом ~2 c. Возвращает `$true`/`$false`. +3. `Invoke-WithProxy -Exe -CmdArgs $args`: + - pre-flight `Test-ProxyReachable` → при неудаче `Write-Error` + return, + **не запускать**; + - проверка `Get-Command ` → при отсутствии понятная ошибка; + - сохранить текущие значения 4 env-переменных; `Set-ProxyEnv`; + `& $Exe @CmdArgs`; в `finally` восстановить прежние значения env + (сессия не остаётся с включённым прокси — поведение симметрично + subshell в bash); пробросить `$LASTEXITCODE`. +4. `function pclaude { Invoke-WithProxy claude @args }` + `function pcodex { Invoke-WithProxy codex @args }` + + **Passthrough аргументов — known footgun PowerShell.** Флаги вида `-p`, + `--model` могут перехватываться парсером PS как параметры обёртки. + Требование: `Invoke-WithProxy` объявляет `[Parameter(ValueFromRemainingArguments)]` + на `$CmdArgs`, обёртки передают `@args` (splatting, не `$args`), внутри — + `& $Exe @CmdArgs`. Обязательные тест-кейсы при реализации: + `pclaude -p "..."`, `pclaude --model ...`, `pcodex --help`, аргумент с + пробелами и с `--`. + +## Обработка ошибок + +| Ситуация | Поведение | +|---|---| +| Прокси `hproxy.local:12334` недоступен | Понятное сообщение, ненулевой код, **запуск не происходит** | +| `claude`/`codex` нет в PATH | Отдельное понятное сообщение, ненулевой код | +| Аргументы с пробелами/спецсимволами | Сохраняются (`"$@"` / `@args`-splatting) | +| `NO_PROXY` | Берётся существующий список платформы как есть (bash: с `::1`/`[::1]`; PS: без) — существующее поведение не меняется | + +## Известные ограничения + +Принимаются осознанно (следствие выбранного convenience-подхода): + +- **Аддитивность.** Голые `claude`/`codex` остаются доступны и идут мимо + прокси без предупреждения. Фича снижает вероятность ошибки, но не + устраняет её — это удобство, не гарантия. +- **TOCTOU.** Pre-flight проверяет доступность только в момент старта. + Для долгой интерактивной сессии `claude` прокси может упасть позже — + обёртка этого не отследит. +- **Pre-flight ≠ доказательство проксирования.** Проверяется открытость + порта, не факт, что инструмент использует прокси (закрыто проверенным + допущением о поведении claude/codex, не самим preflight). + +## Деплой + +Ноль новых установочных шагов. Оба целевых файла уже доставляются (stow / +chezmoi) и подключены в shell-профилях. `run_`-скрипты chezmoi не требуются. + +## Тестирование + +Зеркалит существующий стиль (`tests/test_install.sh` — простой bash + +assert-хелперы, без bats). + +Новый `tests/test_proxy_launchers.sh`: + +- `_proxy_run` при недоступном прокси не вызывает бинарник и возвращает + ненулевой код (мок проверки доступности + мок `command`/PATH); +- proxy-env реально выставлены в дочернем процессе и **не** протекают в + родительский shell; +- проброс кода возврата дочернего процесса; +- аргументы с пробелами и `--` доходят до бинарника без искажений; +- **регресс `proxy_on`**: после рефактора на общий хелпер `proxy_on` + экспортирует ровно тот же набор переменных (включая lower-case дубли и + тот же `NO_PROXY`), что и до рефактора — снимок до/после; +- `shellcheck` на `bash/.bash_functions`. + +Windows-часть — без авто-тестов (нет CI под Windows). Ручной чеклист в +плане: `pclaude`/`pcodex` запускаются; при остановленном прокси fail-fast +срабатывает; env не остаётся в сессии после выхода (в т.ч. после Ctrl-C — +если `finally` не отработал, прокси остаётся включённым: безопасное +направление отказа); passthrough-кейсы `pclaude -p "..."`, +`pcodex --help`, аргумент с пробелами; `proxy_on`/`proxy_off` после +рефактора ведут себя как прежде.