Ветка installer/a61-live-verification (agy, коммит b0644a3) частично
пересекалась с HUB-1, частично добавляла то, чего в HUB-1 не было. Пункты
взяты по одному, дубли — нет.
## Взято
1. verify_multi_provider_router.py проверял изоляцию пути на ID "ag-w2" —
на живой машине владельца это существующий подключённый профиль, и
"assert not pdir.exists()" падал не из-за бага, а потому что каталог
реального аккаунта и так был на месте (код возврата 12). ID заменён на
заведомо не боевой "ag-probe-isolation-test".
2. HermesHubSetup.cs: CreateStartMenuShortcut/RemoveStartMenuShortcut не
уважали HERMES_HUB_NO_REGISTRY — переменная гасила запись в реестр (A4),
но ярлык в настоящем меню Пуск изолированные тесты всё равно писали.
Добавлена та же проверка, что уже стоит перед записью в реестр. Заодно
LOCALAPPDATA читается из окружения раньше SpecialFolder — расхождение
найдено живым прогоном.
3. test_installer.py: /silent-тест линковался на venv настоящей машины
junction'ом (Windows) или symlink'ом вместо пустых touch-файлов — раньше
проверка живых Win32-зависимостей ничего по сути не проверяла.
4. update_manager.py: запасной перебор известных имён установщика
(hermes-hub-setup.sh/install-linux.sh/HermesHubSetup.exe) до отката на
.zip — подстраховка на случай расхождения определения платформы.
5. release_gate.py: --assets проверял только присутствие файлов. Добавлена
нижняя граница размера (усечённая сборка, найдено вживую) и сверка
SHA-256 каждого установщика с локальным checksums.txt — до всякой
публикации. Своя реализация (agy: только HermesHubSetup.exe, только
argparse-обвязка, несовместимая с --publication-only из HUB-1), но идея
и обе живые находки — его. Проверено полным циклом: собран настоящий
dist/hermes-hub-setup.sh, посчитаны настоящие контрольные суммы,
--assets прошёл на них 12588887 байт, SHA-256 сошёлся.
## Не взято — уже есть шире в HUB-1
- security_guard.py: точечный "$HOME" в тексте команды вместо конвейера
(диалект по команде, ${HOME}, %USERPROFILE%, fail-closed) — версия HUB-1
шире и уже зелёная на настоящем Windows CI.
- test_a59: заглушка os.getuid без проверки ветки Windows (taskkill/wmic) —
версия HUB-1 параметризована на обе ветки.
- Скомпилированные .exe — не переношу: пересборка на Windows после этого
коммита, здесь compилятора нет.
Тесты: 780 -> 781 passed, 2 skipped, 4 deselected. ruff check . чисто.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Найдено и проверено живым прогоном на этой машине (реальный GTK-рабочий
стол, реальные профили agy) — не по чтению кода.
1. Значок .desktop-записи был .ico. Измерено:
GdkPixbuf.Pixbuf.new_from_file на HermesHub.ico падает с "Compressed
icons are not supported". .desktop-файл с нерендерящейся иконкой меню
приложений и файловый менеджер просто показывают пустым — без ошибки,
молча. PNG в тех же ассетах уже был и загружается (проверено: 256x256).
Подставлен app_icon_256.png.
2. Хаб нельзя было остановить иначе как из терминала. На Windows сервер
стартует из HermesHubWeb.exe, который держит значок в системном трее —
оттуда «Exit» останавливает процесс. На Linux сервер остаётся в фоне
после закрытия окна браузера (так и задумано — не переустанавливать
каждый раз), но ни кнопки в интерфейсе (её нет ни на одной платформе),
ни трея, ни пункта меню не было вовсе. launcher/hermes-hub-stop.sh —
недостающий эквивалент «Exit из трея»: доступен из меню приложений
через собственный .desktop-пункт, использует ту же функцию, что и
install/uninstall. Проверено живым прогоном: сервер запущен, остановлен
через новый лаунчер, curl после этого получает connection refused.
3. uninstall-linux.sh не останавливал работающий хаб перед удалением файлов
— та же причина, что install уже чинил для установки: с
--purge-user-data это ещё и rm -rf каталогов, на которые у живого
процесса открыты файловые дескрипторы. Измерено: сервер, запущенный в
песочнице, оставался в списке процессов после uninstall-linux.sh до этой
правки. Добавлена та же остановка, тем же кодом.
4. stop_running_hub была вписана отдельно в install-linux.sh и
uninstall-linux.sh — две копии, которые разошлись бы при правке одной
незамеченной для другой. Вынесена в installer/lib_stop_running_hub.sh,
источается обоими скриптами и новым лаунчером остановки.
Всё проверено дважды: прямым запуском install-linux.sh/uninstall-linux.sh
в изолированной песочнице (не ~/.hermes) и через собранный
dist/hermes-hub-setup.sh — тот самый файл, который уходит в релиз.
Тесты: 738 -> 740 passed, 2 skipped, 4 deselected. Новый тест на .ico падает
на прежней версии install-linux.sh (проверено git stash) и проходит после
фикса. ruff check . чисто.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Найдено сверх задания и сверх аудита.
Каждый прогон Release Pipeline завершался ошибкой — все пять последних,
включая тег текущего релиза v0.1.3-b1. Причина та же, что у красного CI: шаг
Release Gate падал на test_a37_isolation_guards и test_a41_clean_install. До
публикации не доходил ни один прогон, релизы выкладывались мимо конвейера.
Отсюда ловушка. release.yml собирает hermes-hub-<версия>.zip и
update_manifest.json, а update_manager ищет строго HermesHubSetup.exe или
hermes-hub-setup.sh. Настоящие релизы содержат установщики и checksums.txt,
то есть собраны не этим конвейером. Пока тесты были красными, конвейер падал
и ничего не публиковал; как только они позеленели, случайная защита исчезла:
первый же тег опубликовал бы "latest" без установщиков, и любое обновление
отвечало бы "В релизе не найден подходящий файл обновления для текущей
платформы".
Ловушка закрыта до публикации: release_gate.py --assets dist проверяет, что
собранный набор содержит установщик и checksums.txt, и падает с названной
причиной и подсказкой про installer/build_installer.*. После публикации
добавлен шаг release_gate.py --publication-only — строгий режим, ради
которого ворота и разделялись.
Сборку установщиков в release.yml не переписывал: проверяется только
настоящей публикацией по тегу, это решение владельца. Конвейер по-прежнему не
доходит до публикации, но падает теперь с честной причиной вместо чужой.
Отчёт перенесён в agents/done/ по конвенции репозитория.
Тесты: 777 -> 778 passed, 2 skipped, 4 deselected. ruff check . чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Джоб на ubuntu упал с «assert 32 == 31» в
test_seq_token_prevents_stale_refresh_clobber. Тест сравнивал поколения до и
после устаревшего вызова, а HubStateStore — процессный синглтон: фоновый
сборщик квот, оставшийся от другого теста, успевает поднять generation между
двумя вызовами. В логе прогона рядом видно как раз такую фоновую попытку.
Падение случайное и зависит от порядка тестов: headless-джоб гоняет pytest без
фиксированного порядка. Тем же объясняется разброс 738/739 в базовом прогоне
до начала работы.
Проверяется теперь инвариант, а не равенство: устаревший ответ отбрасывается
ровно один раз, и состояние не откатывается назад. Пять полных прогонов со
случайным порядком — 777 passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
P1 после зелёного main.
1. CI-матрица. Обе джобы стояли на windows-latest, и это дорого обошлось:
инвариант A37 не держался на Windows, а четыре теста молча предполагали
Linux. Прогон на одной системе не показывал ни того, ни другого. Проект
работает на Linux и активно получает Linux-правки — теперь обе системы
проверяются одинаковым набором.
2. Zip-slip из аудита НЕ ВОСПРОИЗВОДИТСЯ — измерено, а не принято на веру.
Архив с "../", с абсолютным путём и с записью-ссылкой распакован через
zipfile.extractall: ничего за пределы каталога не вышло, абсолютный путь
стал относительным, "../" схлопнулись, а запись-ссылка легла обычным
файлом. CPython санирует пути сам.
Но это свойство реализации, а не обещание формата, и распаковка идёт в
корень установки. Граница сделана собственным инвариантом: каждая запись
проверяется до записи на диск, отклоняются абсолютные пути, выход через
"..", ссылки и записи не-файлового типа. Инвариант закреплён тестом, а не
оставлен на усмотрение стандартной библиотеки.
Тесты: 776 -> 777 passed, 2 skipped, 4 deselected. ruff check . чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
P0 аудита, каждый сначала подтверждён исполнением, а не принят со слов.
1. Release Gate объявлял проверку хеша, которой не было. Печаталась строка
PACKAGE_HASH_VERIFIED=True при том, что hashlib в scripts/release_gate.py
не вызывался ни разу: скачивались байты 0-10 через заголовок Range, и
этого хватало, чтобы счесть хеш проверенным. «Проверенным ассетом» при
этом оказывался первый в списке — checksums.txt, а не пакет.
2. Ворота публикации были fail-open. Измерено в трёх условиях: полный обрыв
сети -> PASS, манифест 404 -> PASS, пакет 404 -> PASS. Ворота пропускали
релиз при любом исходе, включая полное отсутствие релиза.
Разделено на офлайновую часть (проверки 1-7: версии, тесты, updater,
статика, секреты, список разрешённых адресов) и Publication Gate: релиз
есть, ассеты есть, пакет скачан ЦЕЛИКОМ, SHA-256 сошёлся с опубликованным
checksums.txt. Публикационные ворота блокируют в режиме публикации
(--publication или HERMES_RELEASE_PUBLICATION_GATE=1); в обычном прогоне
CI, где релиза для ветки нет и быть не должно, результат сообщается как
есть и не блокирует. Неизмеренное называется причиной, а не выдаётся за
проверенное. Проверено на живом релизе v0.1.3-b1: два пакета скачаны
целиком, хеши сошлись.
3. POST /api/action на loopback принимал межсайтовые запросы. Токен там не
требуется, а действие меняет состояние: удаляет учётные данные, чистит
аккаунты, переключает маршрутизацию, запускает входы OAuth. CORS от этого
не защищает — он мешает прочитать ответ, а не отправить запрос. Измерено
на конфигурации по умолчанию: POST с Content-Type text/plain уходит
кросс-сайтом без предварительного запроса, request.json() разбирает тело
независимо от Content-Type, и запрос с Origin чужого сайта без токена
доходил до исполнителя действий.
Проверяется Sec-Fetch-Site, при его отсутствии — Origin против адреса
запроса. Собственный интерфейс, адресная строка и не-браузерные клиенты
работают как раньше. Защита распространена на все пять небезопасных
методов, не только на /api/action.
4. pricing fallback: safe_load вместо safe_dump. dump сериализовал текст
обратно в строку, проверка isinstance(data, dict) не выполнялась никогда,
таблица цен не загружалась ни разу, а except это глушил.
5. Симуляция Linux в тесте stop_running_hub падала на Windows: os.getuid там
не существует.
Тесты: 756 -> 776 passed, 2 skipped, 4 deselected. ruff check . чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Прогон на Windows-раннере показал, что причин красного CI больше двух.
Четыре падения — не в продукте, а в допущениях тестов, зашитых под Linux.
1. test_a41 читал вывод скрипта в кодировке системы. Скрипт теперь пишет
UTF-8, а родитель на Windows читал трубу как cp1252 и разваливался на
UnicodeDecodeError, оставляя proc.stdout равным None. Кодировка задана
явно с обеих сторон трубы.
2. test_p0_3_stop_running_hub знал только про ветку Linux: os.kill по списку
от pgrep. На Windows процессы останавливает taskkill по списку от wmic,
os.kill не вызывается — тест падал на пустом списке убитых. Инвариант же
один для обеих веток: чужой процесс хаба останавливается, собственный
PID не трогается. Теперь он проверяется на обеих.
3-4. Оба теста установки подсовывали bash-скрипт hermes-hub-setup.sh. На
Windows выбирается HermesHubSetup.exe, и установка честно отвечала «в
релизе не найден подходящий файл обновления для текущей платформы».
Установщик теперь берётся под ту систему, на которой идёт прогон.
Проверка сообщения об ошибке смотрит на то, назван ли код возврата, а не
на склонение: ветки формулируют «код 3» и «кодом 3», инвариант один.
Тесты: 755 -> 756 passed, 2 skipped, 4 deselected. ruff check . чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Оба красных 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>
Аудит A59. Механизм показа и загрузки собран верно, но три вещи говорили
владельцу неправду.
Запись о применённом обновлении делалась ДО запуска установщика. Установщик
падал, запись оставалась, и при следующем старте хаб писал в журнал «успешно
обновлён», а интерфейс показывал тост об успехе — про версию, которая не
установилась. Теперь «было» снимается до установки (иначе после подмены файлов
прежняя сборка совпадёт с новой), а сама запись делается только на путях успеха.
Причина отказа .sh-установщика была пустой: «Установка не удалась: » без единого
признака. Установщик может завершиться, не сказав ни слова, поэтому в сообщение
добавлен код возврата.
Отмена не смотрела, что происходит. Действие cancel_update открыто в HTTP-API, и
вызов на этапе установки чистил staging вместе с исполняемым в этот момент
файлом, отвечая «отменено» поверх продолжающейся установки. Отмена теперь
принимается только на проверке и загрузке, отказ называет причину, интерфейс
возобновляет опрос вместо замершего окна.
Полоса хода при неизвестном размере заполнялась целиком: текст рядом честно
писал «Н/Д: сервер не сообщил размер», а полная полоса читалась как «готово».
Заменена бегущим отрезком на всех этапах с неизвестной долей.
Обработчик хода при неизвестном размере получал выдуманный ноль на каждом чанке —
теперь старая форма обработчика в этом случае просто не вызывается.
Возвращены комментарии ревьюера, снятые в ходе задания: про running_commit как
единственный признак живого кода, про версию только из API, про «отсутствие
суммы — не разрешение», про ожидание установщика и порядок перезапуска.
Тесты: 723 passed, 2 skipped (было 718/2). Добавлены проверки провала установки,
отказа в отмене и честной полосы. ruff чисто, релизный гейт пройден.
НЕ СДЕЛАНО и требует живого прогона: на Linux install-linux.sh снимает сам хаб,
пока тот ждёт установщик, поэтому schedule_restart не выполняется. Отката для
путей .sh и .exe по-прежнему нет.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDLuenXmGjWaS2rs8E72En
Проверено исполнением на настоящем ~/.local/bin/agy:
- SHA-256 бинарника до и после определения состояния совпал (файл не тронут);
- на живом файле получено check_active «Проверка на месте», версия 1.1.24;
- сигнатуры детектора побайтово совпадают с гейтом open-antigravity-patcher;
- «определить не удалось» отделено от «не пропатчен»;
- опроса в цикле не добавлено (setInterval/setTimeout в диффе app.js нет);
- ruff чисто, 721 passed / 2 skipped (порог задания — 704).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G1hLjqc1n8grdmhb8JiDte
Владелец: «в программе нет аккаунтов. я добавил первый… аккаунты так и не
появились». При этом вход проходил, и agy отдавал одиннадцать моделей.
Список аккаунтов строится по конфигурации маршрутизатора. Вход через терминал
завершается своим путём, мимо add_account, и записи в конфигурации не создаёт:
учётные данные на диске, get_profile_status отвечает «подключён», а в интерфейсе
пусто. Воспроизведено исполнением — профиль с ключами agy и нулём записей в
конфигурации не виден нигде.
Теперь при успешном завершении входа профиль регистрируется, и только потом
идёт обновление состояния. Неудача регистрации возвращается отказом с причиной,
а не молчаливым успехом: аккаунт, о котором сказано «подключён», но которого
нигде нет, — та же слепота, что и зависший мастер.
Проверено исполнением до и после правки: было «профилей в конфиге: 0, видно:
ничего», стало «профилей: 1 [ag-5], видно: antigravity[ag-5]».
708 passed, 2 skipped; ruff чисто; релизный гейт пройден.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец не нашёл настройку. Она была в разметке, но её не было на экране.
arrangeSettingsPanels пересобирает настройки по жёсткому списку
идентификаторов, переносит перечисленные строки в новые карточки, а исходную
удаляет целиком — вместе со всем, чего в списке нет. Новое поле попало под
удаление и просто перестало существовать.
Добавлена группа «Сеть и доступ» с полем прокси. Название уточнено до
«Прокси / VPN для провайдеров»: владелец называет это впном, и искать он будет
по этому слову.
Устройство, которое так теряет настройки, тоже исправлено: строки, не попавшие
ни в одну группу, собираются в карточку «Прочие настройки», а не выбрасываются.
Забыть настройку в списке всё ещё можно, потерять её с экрана — уже нет.
Обращение к отсутствующему элементу защищено: опечатка в списке больше не
роняет сборку экрана целиком.
Четыре теста закрывают это: каждый перечисленный идентификатор существует в
разметке, поле прокси сгруппировано, остаток забирается до удаления карточки.
704 passed, 2 skipped; ruff чисто; релизный гейт пройден.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец: «справа постоянно выходят статусы, прям без остановки. я вообще
ничего не вижу за ними».
Это был не таймер, а замкнутый круг. Отрисовка настроек запускала опрос
состояния сжатия; executeAction на успехе вызывал fetchSnapshot; тот снова
перерисовывал настройки — и так без конца. Интерфейс сам себя кормил запросами
к серверу, показывая на каждом обороте два тоста: на запрос и на ответ. Тем же
путём заливал «poll_native_auth» во время входа.
Опросы, которые запускает сам интерфейс, а не владелец, теперь молчат и не
дёргают снапшот — второе и разрывает круг. Отказ опроса показывается там, где
его запросили: у мастера входа для этого своя область сообщений.
Состояние сжатия запрашивается при открытии экрана настроек и по кнопке, а не
при каждой отрисовке.
700 passed, 2 skipped; ruff чисто; релизный гейт пройден.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Вход через терминал прошёл: agy запустился в изолированном каталоге ag-5 и
опознал аккаунт владельца. Отказал Google: «Eligibility check failed: not
currently available in your location». Проверка смотрит на адрес выхода.
У владельца есть узлы 3x-ui в разных странах. Поэтому обход делается выходом
через разрешённую страну, а не патчем чужого бинарника: ничего не ломается при
обновлении Antigravity и не выполняется сторонний код.
Адрес задаётся общий в настройках и отдельный на профиль — разным аккаунтам
может требоваться разная страна. Применяется к запросу каталога моделей, к
вызовам моделей и к сценарию входа в терминале.
Пишутся и заглавные, и строчные имена переменных: Go читает HTTPS_PROXY,
многие библиотеки — https_proxy; ALL_PROXY нужен для socks5.
Адрес проверяется по существу, а не по схеме: приписать socks5:// можно чему
угодно, и тогда мусор выглядел бы принятым, а обращения провайдера молча
ломались бы без внятной причины.
695 passed, 2 skipped; ruff чисто; релизный гейт пройден.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Мастер честно сообщил: «Терминал /usr/bin/xfce4-terminal завершился сразу с
кодом 1, окно не открылось» — новая проверка запуска сработала.
Причина в моей же правке. Терминал запускался с HOME, подменённым на каталог
профиля, а клиенты X11 берут ключ авторизации из ~/.Xauthority: в
agy_profiles/ag-5 такого файла нет и быть не может, поэтому подключиться к
дисплею терминал не мог. Подменять HOME терминалу и не требуется — это делает
сценарий входа, уже внутри окна, перед самым запуском agy.
Заодно устранено побочное действие в запросе: сценарий входа создавался внутри
функции ПОИСКА терминала. Теперь его готовит start_native_agy_login, а поиск
только отвечает на вопрос и ничего не создаёт.
Владелец не мог понять, обновился ли хаб: на Linux строка сборки в боковой
панели была пуста, потому что заполнялась из панели обновлений, а та
подтягивается только при открытии. Теперь номер берётся из снапшота и
показывается вместе со временем запуска процесса.
Показывается running_commit — снятый ОДИН РАЗ при старте процесса. Поле commit
читается из манифеста на диске при каждом запросе, поэтому переживший
обновление процесс рапортует им свежий номер при старом поведении; на этом я
уже спотыкался при разборе окон консоли. Снятое при старте значение отвечает на
настоящий вопрос: какой код сейчас в памяти.
678 passed, 2 skipped; ruff чисто; релизный гейт пройден.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Мастер сообщил «Графический дисплей не обнаружен», хотя окно хаба было открыто
на рабочем столе владельца. Причина: хаб запускается через nohup и наследует
окружение той оболочки, из которой его запустили. Запуск по SSH или службой
оставляет процесс без DISPLAY.
Наследование не единственный источник. Измерено на сервере владельца:
loginctl show-user ochenstarik -p Display даёт c1, а show-session c1 —
Type=x11, Display=:10, Active=yes. Спросить у системы честнее, чем сдаться.
Теперь дисплей ищется сначала в окружении, затем у systemd, и найденное
значение передаётся в окружение терминала — иначе окно не открылось бы даже
при верном обнаружении. Отказ остаётся правомерным, только когда графического
сеанса не знает и systemd; в сообщении перечисляется всё проверенное, включая
сам loginctl.
XAUTHORITY не выставляем: клиенты X11 по умолчанию берут ~/.Xauthority того же
пользователя, а хаб работает под ним же.
674 passed, 2 skipped; ruff чисто; релизный гейт пройден.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец спросил, откуда 25 каталогов профилей, когда заводил единицы.
Проверено исполнением: обращение к get_profile_dir создавало каталог. Спросили
четыре пути — появились четыре каталога.
В коде зашит список «стандартных» слотов — ag-orch-primary, ag-orch-fallback,
ag-1..ag-20, ag-w1..ag-w10 — в двух местах: agy_subprocess и
model_discovery_service. Любой обход этого списка материализовал их все. Отсюда
ag-w1..ag-w4, ag-cold-*, ag-spare-* — владелец их не создавал.
Спросить, где профиль жил бы, и завести его — разные действия. Создание теперь
запрашивается явно, create=True, и это делают только те, кто действительно
заводит профиль.
Тесты и сценарий проверки, опиравшиеся на побочное создание, приведены к новому
поведению: они проверяют изоляцию пути, а не то, что каталог возник сам.
669 passed, 2 skipped; ruff чисто; релизный гейт пройден.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец увидел «Терминал запущен (/usr/bin/x-terminal-emulator) для слота
ag-6», но окна не появилось. На сервере в это время висел зомби
[xfce4-terminal] <defunct>: терминал стартовал и немедленно умирал.
Причин три.
x-terminal-emulator на Ubuntu указывает на xfce4-terminal.wrapper, а
xfce4-terminal держит один процесс на сеанс: новый вызов передаёт задание уже
работающему экземпляру и завершается. Нужен --disable-server. Конкретные
эмуляторы теперь пробуются раньше обёртки над альтернативами.
При такой передаче команда выполняется в окружении СТАРОГО экземпляра, и
подменённый HOME не применяется — вход ушёл бы в настоящий домашний каталог
владельца мимо всей изоляции слотов. Теперь терминал запускает сценарий,
который задаёт HOME сам, а не полагается на наследование.
С ключом -e окно закрывается вместе с командой, и причину отказа прочесть
нельзя. Сценарий печатает код возврата agy и ждёт нажатия клавиши. На Windows
по той же причине cmd /k вместо /c.
Отдельно: возврат Popen об открытии окна не говорит ничего, а мастер выдавал
его за успех. Теперь запуск подтверждается тем, что процесс прожил хотя бы
секунду; мгновенное завершение сообщается с кодом возврата.
Тесты подменяли глобальный os.name, а его читает pathlib при выборе класса
пути: на Windows это роняло и проверяемый код, и сам pytest, как только в
ветке для Linux появилась работа с файлами. Заменено явной проверкой системы.
669 passed, 2 skipped; ruff чисто; релизный гейт пройден.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверено исполнением, а не по отчёту.
Работает. Профиль, где лежит только файл, записанный agy, признаётся
подключённым, и хаб этот файл не переписывает — главный критерий приёмки
выполнен. Поиск терминала честный: перечисляет проверенных кандидатов, а
DISPLAY, WAYLAND_DISPLAY, XAUTHORITY и DBUS_SESSION_BUS_ADDRESS внесены в
список разрешённых переменных, иначе окно терминала не открылось бы. На
сервере владельца найдены x-terminal-emulator, gnome-terminal,
xfce4-terminal, xterm.
Исправлено два дефекта.
1. Почта после входа искалась в google_accounts.json, auth.json и id_token.
У свежего слота первых двух нет, а antigravity-oauth-token у владельца
занимает 505 байт — id_token туда не помещается. Последняя попытка
разбирала токен доступа как JWT, но ya29-токен Google не JWT и claims не
несёт. Пустая почта отключает проверку двойников, и рост номеров слотов,
починенный в 9641957, вернулся бы. Теперь почта запрашивается у UserInfo —
тем же способом, каким её узнаёт браузерный вход. Отказ сети вход не
роняет: аккаунт подключён, почта Н/Д.
2. В существующем тесте test_seq_token_prevents_stale_refresh_clobber строгая
проверка была заменена на нестрогую. Прогнал исходную пять раз подряд и в
полном наборе — проходит. Ослабление было лишним, вернул; добавленную
исполнителем проверку seq оставил, она по делу.
Не выполнено исполнителем: живая проверка входа на сервере (P0-7.1) — вместо
неё двенадцать модульных тестов. Вход требует участия владельца, поэтому
проверить его сам не могу.
Неточности отчёта: «Release Gate 16/16» — это счёт внутри второго раздела, а
не итог гейта; обращения к UserInfo API в коде не было, оно добавлено здесь.
661 passed, 2 skipped; ruff чисто; релизный гейт пройден.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверено исполнением на сервере владельца, а не по отчёту.
Работает. Сжатие вызывается в настоящем пути запроса (local_adapter.py:168) —
разрыв, ради которого писалось задание, закрыт. Замер на живом компрессоре:
5667 токенов на входе, 624 на выходе, 0,11x, экономия 5043 токена за 127,6 с.
Исправлено четыре дефекта.
1. /props и /tokenize запрашивались по адресу с суффиксом /v1. У llama.cpp они
живут в корне: измерено, /props → 200, /v1/props → 404, то же с /tokenize.
Адаптер передаёт супервизору именно адрес с /v1, поэтому счёт токенов молча
падал на посимвольную оценку, и порог сжатия считался от выдуманного числа.
Адрес нормализуется.
2. Заявленные «сто процентов сохранения фактов» модель не даёт. Замер: 37 из 38,
97,4%, потерян 001cd1f. Сто процентов получались дописыванием недостающих
фактов списком — механизм верный, но измеренное число подменялось
исправленным, а «(100%)» было вписано в сообщение текстом. Теперь полнота
самой модели сохраняется отдельно и показывается владельцу: иначе ухудшение
модели осталось бы незамеченным.
3. Итог сжатия считался посимвольно (длина / 3.5) и подавался рядом с настоящим
числом токенов на входе. Теперь пересчитывается токенизатором сервера, а
недоступность токенизатора помечается признаком оценки.
4. Порт 8082 был зашит запасным адресом в двух местах вопреки прямому запрету в
задании. Профиль без адреса теперь даёт состояние «не настроен» с причиной,
а не молчаливый стук в 8082.
Не выполнено исполнителем: проверка на живом сервере (P0-6.1). Тест на неё
пропускается как негерметичный, замеры выше сделал ревьюер.
649 passed, 2 skipped; ruff чисто; релизный гейт пройден.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Реализован запуск agy в терминале с изолированным HOME (x-terminal-emulator, gnome-terminal, konsole, xfce4-terminal, tilix, alacritty, kitty, terminator, urxvt, foot, xterm / Windows wt, cmd)
- Опрос появления antigravity-oauth-token вместо ожидания процесса терминала
- Защита занятого слота от перезаписи без подтверждения
- Честное извлечение email без выдумывания identity и предотвращение дубликатов слотов
- Сохранён браузерный OAuth в качестве запасного пути
- Добавлены 12 тестов в test_a57_agy_native_login.py, 640 тестов проходят
В карточке владельца рядом стояли «Проверен: не работает — Please sign in» и
«Получено 11 моделей · 13:25:53». Источники разные: красная строка берётся из
состояния проверки, список — из кэша каталога, и обновляются они независимо.
Каталог был получен позже отказа, то есть провайдер с тех пор ответил, а
карточка продолжала утверждать обратное.
Теперь, если каталог получен без ошибки и позже неудачной проверки, вердикт
показывается как устаревший с предложением проверить заново. Объявлять аккаунт
рабочим не за что — проверка после этого не выполнялась, и выдумывать её
результат нельзя.
Отдельно установлено: поле commit в /api/health читается из файла на диске при
каждом запросе, поэтому устаревший процесс рапортует свежий коммит. Различать
сборки по нему нельзя.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец видел «сохранение аккаунта и запуск проверки» и ждал. Зависанием это
не было: действие честно дожидалось проверки у провайдера. Для Antigravity она
идёт через CLI и в худшем случае складывается из 90 с на захват замка профиля,
65 на каталог моделей и 90 на пробный вызов — около четырёх минут молчания при
обещанной в интерфейсе «минуте на этап».
Сохранение учётных данных и назначение роли занимают миллисекунды. Теперь
действие возвращается сразу, а опрос провайдера ставится в фон; карточка
обновляется, когда он закончится, — снапшот и так опрашивается по таймеру.
Провайдеры с ключом поведения не меняют: их подключение проверяется
предварительной проверкой до сохранения и возвращается сразу, как требует A54.
Если фоновая служба не работает, проверка по-прежнему выполняется на месте —
иначе результата не будет вовсе.
Надпись в мастере исправлена: обещание «до минуты на этап» не соответствовало
действительности.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Установка на сервере владельца прервалась на проверке: «No module named
'fastapi'». Причин две, и обе в установщике.
Первая: запуск через sudo. Установка пользовательская — всё ложится в
$HOME/.hermes и $HOME/.local/bin, root не нужен. Под sudo домашним каталогом
становится /root, венв Hermes там не находится, и установщик уходит на
системный python. Копия при этом ложится в /root/.hermes, где владелец её не
видит. Теперь запуск через sudo распознаётся и отклоняется с объяснением;
осознанный обход остаётся через HERMES_ALLOW_ROOT=1.
Вторая: системный python в Ubuntu 24.04 помечен EXTERNALLY-MANAGED (PEP 668) и
отклоняет pip install — и обычный, и с --user. Установщик обе неудачи проглатывал
(|| true) и продолжал работу до отказа на проверке. Теперь при защищённом
системном python создаётся собственное окружение $HERMES_HOME/venv, а неудача
установки зависимостей прекращает установку с внятным кодом возврата.
--break-system-packages не применяется: имя флага не преувеличивает.
Пусковик научен той же ветке — иначе запуск уходил бы на системный python,
где зависимостей нет и быть не может.
Проверено исполнением на сервере под непривилегированным пользователем:
окружение создано, зависимости установлены, HERMES_HUB_LINUX_VERIFY_OK,
запуск через sudo отклонён с кодом 3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
agy 2.0 читает учётные данные не из .gemini/oauth_creds.json — это формат
Gemini CLI. Свой токен он берёт из .gemini/antigravity-cli/antigravity-oauth-token
в виде {"auth_method": ..., "token": {...}}. Хаб этот файл никогда не создавал,
поэтому «Авторизация успешно завершена» соседствовала с ответом
«Please sign in to view available models».
Установлено сравнением рабочего профиля владельца с неработающим: оба имели
oauth_creds.json одинакового размера, различие было только в этом файле.
Значение auth_method («consumer») взято из рабочего профиля, а не выведено
из общих соображений.
Срок годности пишется в обоих видах: expiry_date в миллисекундах для формата
Node и expiry строкой RFC3339 для Go-шного oauth2.Token.
Заодно устранён рост номеров профилей: слот выбирался до входа, когда почта
ещё неизвестна, и повторный вход тем же аккаунтом занимал очередной свободный
слот — один аккаунт владельца расползся на ag-2, ag-3, ag-4. Теперь после
опознания почты учётные данные возвращаются в слот, который этот аккаунт уже
занимает. Функция проверки двойников в проекте была, но её никто не вызывал.
Версия поднята до 0.1.3, чтобы отличать сборки на глаз. Проверка версии в
тестах сверяется с исходником, а не с записанным числом.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверено ревьюером исполнением: подключение с выбором роли кладёт
аккаунт в её цепочку (developer-1 → ['openrouter-1']). Раньше выбор роли
до цепочки не доходил, и ни один аккаунт владельца ни в одной цепочке не
состоял.
Роль на карточке берётся из живой цепочки: role_assignments.get(pid, [])
вместо подстановки догадки. Таблица DEFAULT_SLOT_ROLES удалена — ссылок
на неё в дереве не осталось.
На маршрутизации показывается «Сейчас ответит: <аккаунт>» либо «никто»
с причиной. В плагине Hermes учитываются вызовы мимо хаба
(record_bypass в четырёх местах, счётчик bypassed_calls_count).
Обратное чтение сделано честно: get_hermes_config_status читает
конфигурацию Hermes ТОЛЬКО на чтение, с отдельным тестом на это.
Разрешение конфликтов с A49 и A50 (ветка A51 отведена от 17b368a):
- action_handler: взят вариант A50 — он надмножество, содержит и
назначение в роль, и защиту слота от чужого провайдера;
- auto_assigner: взят вариант A51 — таблица-догадка удалена;
подпись llama.cpp от A50 сохранена, она вне конфликта;
- settings_service, unified_health, app.js: обе стороны, правки
дополняют друг друга;
- тест свежести памяти: взята строгая проверка, вариант A51 молча
пропускал отсутствие записанного коммита.
Две правки ревьюера по итогам слияния:
- в app.js при сложении потерялась закрывающая скобка блока настроек;
- в add_account сохранение учётных данных вызывалось дважды, и второй
вызов обращался к auth_data, которой у уже авторизованного аккаунта не
существует. Перевод аккаунта в другую роль падал с ошибкой, хотя ключ
вводить не требуется. Дубль убран, оба теста A51 проходят.
554 passed, 1 skipped, ruff clean, релизный гейт 10/10 и на конфигурации
владельца, и на пустой.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверено ревьюером исполнением, все восемь замечаний владельца закрыты:
Чужой слот. Раньше add_account с profile_id=ag-w1 для nvidia возвращал
ok=True и клал аккаунт в слот Antigravity. Теперь отказ с причиной:
«Слот ag-w1 не принадлежит провайдеру nvidia». Свой слот принимается,
идентификатор выдаётся верный.
Группировка по провайдерам восстановлена: скрытие заголовков групп,
добавленное в A48 (display:contents + display:none), убрано.
Автоматическая проверка запускается при старте веб-сервера
(_start_background_refresh), состояние и списки моделей больше не ждут
ручного нажатия.
Облачные модели Ollama: эндпоинт не выдуман — ревьюер проверил запросом,
https://ollama.com/api/tags отвечает 200 и отдаёт 19 моделей
(gpt-oss:20b, kimi-k2.6, glm-5.1 и другие).
Локальный провайдер подписан llama.cpp вместо «Локальный сервер».
Показ хода при долгом опросе честный: «может занять до минуты на этап».
Конфликты с A49 разрешены сложением: правки дополняют друг друга —
поле пути к хранилищу Obsidian и поле периода проверки аккаунтов,
стили вкладки скиллов и стили групп провайдеров. В тесте свежести памяти
взят вариант A50: коммит извлекается из файла, а не зашит.
545 passed, 1 skipped, ruff clean, релизный гейт 10/10 и на конфигурации
владельца, и на пустой.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A44: отчёт A40 приведён в порядок. Размер контекста замеров указан
(-c 32768), штатный режим владельца вынесен отдельно: 196608, 13,6 ток/с,
25 490 МиБ — совпадает с независимым замером ревьюера (25 488). Столбец
VRAM пересчитан на потребление процесса через --query-compute-apps.
llama-swap описан; ревьюер подтвердил, что он установлен и работает на
порту 8090 с десятью моделями. Огрызок nemotron убран.
A49: роль skill-doctor добавлена (ролей стало 14), вкладка «Скиллы»,
обнаружение хранилища Obsidian по каталогу .obsidian с проверкой доступа
на запись.
Правка ревьюера: gguf перенесён из основных зависимостей в дополнение
benchmarks. Он используется только стендом замеров и продуктом не
импортируется, а в основных зависимостях заставлял каждую установку хаба
тянуть библиотеку разбора GGUF. Тест стенда получил importorskip: без
gguf он ронял СБОР всех тестов, а не пропускал себя.
526 passed, 1 skipped, ruff clean, релизный гейт 10/10 и на конфигурации
владельца, и на пустой.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A48 (Codex): вёрстка по макетам. style.css изменён на 178 строк, добавлен
workspace.js, приложены 79 скриншотов — до, после, макеты для сверки.
Проверено глазами: логотип, шапка с поиском, панель инструментов холста,
карточки узлов с моделью и аккаунтом, читаемые подписи связей, инспектор с
вкладками, три карточки внизу. Выдуманных чисел из макета нет, пустые
состояния честные.
A47: хранилище AI-Memory под git локально, память проекта приведена к
действительности, worklog заполнен, составлен перечень агентов сервера.
A45: три кандидата замерены. Файлы и размеры сверены по диску, контрольная
сумма Qwen3-Coder пересчитана независимо и совпала. Скорость 109,6 ток/с
на 64К независимо НЕ перемерена: свободно 1,9 ГБ видеопамяти, проверка
потребовала бы остановить рабочий кодер владельца.
A42: подключение OpenRouter, NVIDIA и Ollama, отдельные ветки обнаружения
моделей, отказ с причиной вместо мнимого успеха.
Конфликт A42 с A41 разрешён в пользу A41: ветка A42 отведена от e7194d3,
до слияния A41 в main, поэтому в ней не было динамических префиксов.
Сохранены _get_prefix и генерация слотов; зашитый provider_slots не взят.
Расширенный список ролей openrouter из A42 принят.
Ожидание теста A42 поправлено: nvidia и nvidia-nim — псевдонимы одного
провайдера с одним адаптером, слоты у них общие.
517 passed, ruff clean, релизный гейт 10/10 и на конфигурации владельца,
и на пустой.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>