Оба красных Windows-джоба CI падали по причинам, воспроизведённым локально.
1. Инвариант A37 не держался на Windows. "rm -rf $HOME/.hermes" проходил
мимо защиты: переменной HOME в окружении Windows нет, expandvars оставлял
"$HOME" как есть, путь переставал быть абсолютным, склеивался с каталогом
проекта и оказывался "внутри разрешённого корня". Зеркальная дыра на
Linux: "%USERPROFILE%\.hermes" и "C:\Windows" проходили так же.
Разбор пути сведён в один конвейер: классификация диалекта shell по самой
команде (а не по системе-хозяину) -> раскрытие распознанных переменных, с
разрешением HOME/USERPROFILE в домашний каталог даже когда их нет в
окружении -> нормализация разделителей -> канонизация -> сравнение с
защищёнными корнями. Каждый несостоявшийся шаг закрывает проход:
непроверяемый путь не считается разрешённым. Через тот же конвейер
пропущены validate_path, is_forbidden_path и is_inside_allowed_root.
2. UnicodeEncodeError ронял verify_multi_provider_router.py на cp1252-консоли
Windows-раннера — падал вывод, не логика. Общий помощник
console_encoding.force_utf8_output ставит UTF-8 на потоки и оставляет
запасной путь, если перекодировать поток нельзя. Той же реализацией
заменён самодельный блок в cli_commands.
Проверено: скрипт проходит 10/10 под PYTHONIOENCODING=cp1252 и ascii.
Новые тесты воспроизводят окружение обеих систем на любой из них и падают
на прежнем guard ровно на дефекте из CI (6 failed), проходят на новом.
Тесты: 739 -> 755 passed, 2 skipped, 4 deselected. ruff check . чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>