Джоб на 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>
Шаг Release Gate падал UnicodeEncodeError'ом на Windows-раннере: отчёт
печатается по-русски, консоль раннера — cp1252. Тот же класс дефекта, что и
в verification-скрипте, и то же лекарство — force_utf8_output до первого
вывода. Проверено прогоном под PYTHONIOENCODING=cp1252.
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>
Тринадцать файлов существовали только на диске ПК владельца, в рабочей
копии, отставшей от origin/main на 122 коммита. Их реализация и тесты
давно влиты: tests/test_a42_provider_connect.py,
test_a49_subagents_skills_memory.py, test_a51_hub_controls_hermes.py,
test_a52_local_models_supervisor_dual.py, test_a55_account_connection.py,
test_a56_context_compression.py и другие. Постановок, объясняющих, что
эти тесты обязаны доказывать, в репозитории не было.
Правило записано в agents/AGENTS.md: задание живёт в репозитории, а не в
переписке и не в личных папках на диске.
A48, A50 и A54 не переносятся: они уже есть на origin под другими именами,
содержимое совпадает с точностью до перевода строки в конце файла.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Задание для серверной сессии Claude (не для agy): довести main до зелёного и
закрыть P0 аудита Hermes Hub. Шаг 1 плана слияния — стабильный Hermes как эталон
переноса.
Причина красного main найдена ревьюером в логе CI, а не по аудиту:
- test_a37_isolation_guards.py:394 — WorkspaceBoundaryGuard пропускает
rm -rf $HOME/.hermes на Windows (провал security-инварианта);
- verify_multi_provider_router.py:63 — UnicodeEncodeError на cp1252 при печати
русского текста.
Остальные P0 (Release Gate hash, publication gate fail-open, /api/action CSRF) —
подтвердить исполнением перед правкой. pricing safe_dump — реальный, в P1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец принял направление: один продукт KAgent, функциональность Hermes
переносится нативно, после parity Hermes архивируется. На переходный период
KAgent доделывается на одном сервере, Hermes — на втором.
Ревьюер проверил обе стороны исполнением, а не по аудиту:
- Hermes: CI на main красный; баг pricing fallback реален (safe_dump вместо
safe_load в telemetry_service.py:164, таблица цен не грузится).
- KAgent (head 131c9b08): лицензии нет; в reasoning-engine/src/server.py
require_operator_secret стоит на управлении аккаунтами, но НЕ на /v1/execute,
/v1/decide, /v1/telemetry — расход провайдера открыт без авторизации.
Поправка к аудиту: последняя активность 18-19 августа, репозиторий замер.
Жёсткий гейт: ключи провайдеров не переезжают в KAgent, пока эти маршруты не
закрыты и это не проверено живым запросом. Сам план миграции положен в
репозиторий как артефакт, чтобы не жил только файлом на столе.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Поставил v0.1.2-b7 в изолированную песочницу (свои HOME и HERMES_HOME, живая
установка не тронута) и запустил обновление на v0.1.3-b1. Хаб умер через шесть
секунд и за 150 секунд не поднялся. Установка при этом не произошла вовсе:
манифест и код остались на 0.1.2.
В задании было написано, что установщик доживает сиротой и файлы обновляет.
Прогон это опроверг. Настоящая цепочка: хаб зовёт установщик через
capture_output=True, то есть читателем его вывода становится сам хаб; установщик
на шаге [0/6] снимает хаб; у трубы не остаётся читателя; следующий echo даёт
SIGPIPE, и установщик умирает на шаге [1/6], не поставив ничего. Проверено
контрольным опытом: тот же скрипт с выводом в файл доходит до конца, с трубой
умирает. Отсюда следует, что перестановкой schedule_restart делу не помочь.
Тем же прогоном найдено второе: deployed_at пишется в момент установки, а не
сборки, и сравнивается с датой публикации релиза. Хаб на b7 при живом b1 ответил
«установлена сборка новее опубликованного релиза» и обновление не предложил.
Переустановил старую сборку — выпал из обновлений молча. Вынесено в P0-5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDLuenXmGjWaS2rs8E72En
A59 влит, база задания указана коммитом. Порог тестов поднят с 725 до 740:
столько в main после слияния с ветками A58 и A59.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDLuenXmGjWaS2rs8E72En
Окно при запуске, полоса хода с честным Н/Д без размера, отмена с удалением
недокачанного файла, обязательная SHA-256 — принято как есть.
Правки ревьюера: запись о применённом обновлении делалась до запуска установщика
и переживала его падение, поэтому хаб рапортовал об успехе версии, которая не
установилась. Отмена не смотрела на этап и сносила staging вместе с исполняемым
установщиком. Полоса при неизвестном размере заполнялась целиком, хотя текст
рядом честно писал Н/Д. Возвращены шесть блоков комментариев, снятых в ходе
задания.
Не сделано и вынесено в A60: установщик снимает процесс, который его запустил и
ждёт, поэтому перезапуск не наступает; отката для путей .sh и .exe нет.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDLuenXmGjWaS2rs8E72En
Аудит 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
A59 довёл обновление до экрана: окно при запуске, видимая загрузка, честное Н/Д,
отмена. Дальше механизм обрывается на самом простом — установщик снимает тот
процесс, который его запустил и ждёт результата, поэтому перезапуск не наступает
никогда.
Корень один на обеих системах. На Linux install-linux.sh снимает всё по шаблону,
никого не исключая. На Windows StopOwnedRuntime строит цепочку предков, но
проверяет её только для дочерних процессов, а сам процесс-цель снимает
безусловно — прочитано по коду, подтвердить исполнением.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDLuenXmGjWaS2rs8E72En
Наработки из новостных сводок оседали в переписке и терялись. Заведён
docs/research/: что рассмотрено для Hub, с вердиктом и причиной по каждому
пункту — отдельно от ARCHITECTURE (что построено).
Разбор Agent Orchestrator: ближайший архитектурный родственник Hub. Перенять
автоматический возврат замечаний исполнителю (у нас его нет — ревьюер правит
руками), ветку и worktree на работника, доску состояния флота; целиком не брать.
Журнал разведки за 31.08–02.09: Fable 5.1 и AgentsView — брать; Hermes v0.21.0 —
сервер обновлять последним (два бага, но 65536 у нас своё, не путать);
Qwen MTP и свежий llama.cpp — на V100 выигрыша нет, проверено в A52.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверено исполнением на настоящем ~/.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
Владелец показал, как это сделано в Cockpit Tools: окно при запуске, видимая
загрузка, останов служб, установка и запуск без участия человека.
Строить заново нечего. Проверено: проверка при запуске уже выполняется, но в
тихом режиме; обработчик хода скачивания в загрузчике уже написан, но его
результат никуда не выводится; останов и перезапуск держатся на schedule_restart
и не проверены на обеих системах.
Внешнее условие: лента релизов отстала на v0.1.2-b7 при установленной 0.1.3 —
пока свежий релиз не опубликован, обновлять не на что.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец: «в программе нет аккаунтов. я добавил первый… аккаунты так и не
появились». При этом вход проходил, и 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>
Владелец теряет время не на сам обход, а на то, что узнаёт о слетевшем патче
случайно, по невнятному отказу посреди работы.
Установлено ревьюером: строки отказа в бинарнике нет — её присылает сервер, но
отказывается работать клиент. Прокси не помогает, измерено на двух странах:
ограничение привязано к аккаунту. Патчер владельца правит четыре байта машинного
кода, подменять в настройках нечего.
Хаб только читает и сообщает; действие остаётся за владельцем и выполняется его
собственным средством. Патчить чужой бинарник хабу запрещено отдельным пунктом.
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 тестов проходят
Хаб сейчас записывает чужой файл учётных данных своим кодом: формат восстановлен
по рабочему профилю владельца и по строкам в бинарнике. Работает, но до
следующего обновления agy — и отказ будет молчаливым.
Проверено запуском: подкоманд login или auth у agy нет, вход только запуском CLI
без аргументов. Значит вход переносится в терминал с HOME на каталог профиля,
а браузерный путь остаётся запасным для удалённого случая.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
В карточке владельца рядом стояли «Проверен: не работает — 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>
Вход. agy читает учётные данные из <каталог профиля>/.gemini/oauth_creds.json.
Хаб этот файл пишет, но при отсутствии токена доступа создавал его пустым, а
сбой записи только заносился в журнал. Владелец видел «подключено», а проверка
отвечала «Please sign in to view available models» — ровно это и случилось с
профилем ag-2 при заново подключённом victor.trushenko@gmail.com.
Теперь вход без токена доступа отвергается с причиной, а несостоявшаяся
запись учётных данных не позволяет считать аккаунт подключённым. Проверено:
данные без токена отклоняются, с токеном проходят.
Версия. Между сборками она не меняется намеренно, а коммит — строка из
шестнадцатеричных цифр, по которой на глаз не отличить старую сборку от
новой. В /api/settings добавлено время установки из манифеста, и оно
показывается рядом с версией: «Hermes Hub Web v0.1.2 · 01.09 04:10».
610 passed, ruff clean, релизный гейт 10/10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Новое сообщение об отказе agy показало причину. У рабочего аккаунта
victor.trushenko@gmail.com каталог получался (11 моделей), а вызов падал:
invalid model selection (--model "gemini-3.7-flash" --effort ""):
gemini-3.7-flash requires --effort (available: low, medium, high)
Каталог agy отдаёт идентификаторы с уровнем усилия внутри имени
(gemini-3.7-flash-high, -medium, -low), а в профиле хранится голое имя.
Вызов уходил с пустым --effort, и agy отказывался работать.
Теперь уровень определяется: если он зашит в имени, отделяется от модели;
если нет — берётся из каталога, предпочтительно medium. Проверено, что
claude-sonnet-4-6 при этом не разбирается ошибочно: -6 уровнем не
является.
Отдельно: восемь профилей Antigravity отвечают «Please sign in to view
available models» при HOME=~/.hermes/agy_profiles/<slot>. Это пустые
заготовки без учётных данных, а не поломка: agy запускается с подменённым
HOME ради изоляции аккаунтов, и в незаполненном каталоге ключей нет.
Подписочные аккаунты Codex и Claude: отказ по API-ключу теперь объясняет,
что такие аккаунты подключаются входом по ссылке, а не ключом. Проверка
обращается к каталогу моделей, где платформенному ключу нужны права
api.model.read, и подписочный токен там получает 401 или 403 при исправном
аккаунте.
610 passed, ruff clean, релизный гейт 10/10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Адрес. Адаптер всегда шёл на 127.0.0.1:11434, получал Connection refused и
помечал аккаунт нерабочим — при том что каталог из девятнадцати облачных
моделей у него получался. У Ollama Cloud локального сервера нет вовсе,
есть только ключ. Теперь при заданном ключе и незаданном адресе адаптер
идёт на ollama.com; явно указанный адрес по-прежнему в приоритете.
Проверено: только ключ даёт https://ollama.com, заданный адрес — его же.
Ключ. Как у NVIDIA и OpenRouter, он читался только из auth_config в
router_profiles.yaml, куда мастер его не кладёт. Добавлено чтение из
хранилища учётных данных.
Лимиты. Провайдер ollama безусловно относился к локальным и получал
подпись «Без ограничений (локальная модель)». Для облачного аккаунта это
неправда: лимиты у него есть. Теперь облачный аккаунт (узнаётся по
наличию ключа) показывает «Н/Д: лимиты не измерены — провайдер не
сообщает их через API».
Отдельно: отказ agy теперь доносит причину. Прежнее «код 1; каталог не
получен» скрывало и текст ошибки, и главное — что agy запускается с HOME,
подменённым на каталог профиля ради изоляции учётных данных. Если вход
делался обычным agy в оболочке, ключи легли в настоящий домашний каталог,
и профиль пуст. Теперь в сообщении и ответ agy, и использованный HOME.
610 passed, ruff clean, релизный гейт 10/10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Каталог NVIDIA публичный: GET /v1/models отдаётся вообще без ключа и
возвращает одни и те же 83 модели всем. Проверено запросом. Полей о
доступности в нём нет — только id, object, created, owned_by. Значит
узнать из каталога, чем может пользоваться аккаунт, невозможно.
Единственный достоверный способ — спросить у самой модели. Недоступная
отвечает «404 Function ... Not found for account <id>» до генерации, то
есть её проверка ничего не стоит; доступная расходует один токен при
max_tokens=1.
Добавлены модуль model_entitlements, действие probe_account_models и
кнопка «Определить доступные модели» в карточке аккаунта для NVIDIA и
OpenRouter. Запуск только по явному нажатию с подтверждением: опрос
тратит вызовы и упирается в ограничения частоты. Результат сохраняется.
Модели раскладываются на три группы, а не на две: доступные, не выданные
аккаунту и НЕ ОПРЕДЕЛЁННЫЕ с причиной. Отвергнутый ключ (401/403),
превышение частоты (429) и сетевой сбой попадают в третью группу —
выдавать их за «недоступно» нельзя. Проверено: без ключа все модели
получают «ключ не задан», с неверным ключом — «ключ отвергнут (HTTP 403)».
610 passed, ruff clean, релизный гейт 10/10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
При проверке подключения ключ читался из window._wiz_token. Переменная
переживает предыдущие попытки подключения и могла оказаться пустой или от
другого провайдера: OpenRouter отвечал «HTTP 401: Missing Authentication
header» при заполненном поле, и причина была не видна.
Прямой вызов validate_connection с ключом даёт «401: User not found», то
есть заголовок формируется верно — дело было в источнике значения.
Теперь ключ и адрес читаются из полей в момент проверки.
609 passed, ruff clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Установщик копировал файлы, но работающий сервер не трогал. Процесс
продолжал выполнять прежний код из памяти, и владелец видел старый
интерфейс при новом номере сборки: раздача статики читает файлы с диска,
а вся логика действий живёт в загруженном модуле. Три сборки подряд
ставились в файлы, но не в работу — отсюда «сброс не работает»,
«очистка не работает», «версия не изменилась».
Добавлен шаг 0: остановка процессов хаба до копирования. Ищутся только
процессы текущего пользователя и только по признакам хаба
(antigravity_provider.router.web, hermes_hub_web_entry). Сначала обычное
завершение, десять секунд ожидания, затем принудительное. Если процессы
всё же остались, установка НЕ прерывается — файлы обновляются, а владельцу
сообщается, что старый код продолжит работать, пока он их не снимет.
В итоговом сообщении сказано, что хаб остановлен и его надо запустить
заново, и дана команда для проверки, что поднялся новый код.
609 passed, ruff clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
agy. Поиск проверял только раскладку Windows (%LOCALAPPDATA%/agy/bin) и
PATH. В Linux утилита ставится в ~/.local/bin, а хаб запускается с
урезанным окружением, где этого каталога в PATH нет. Владелец видел «agy
executable not found» при установленной и работающей утилите — which agy
находил её по адресу /home/ochenstarik/.local/bin/agy.
Добавлены стандартные места Linux: ~/.local/bin, /usr/local/bin, /usr/bin,
/snap/bin. Сообщение об отказе перечисляет проверенные пути. Отдельно
различается «нет доступа к каталогу» и «файла нет».
Ollama Cloud. Мастер требовал адрес локального сервера, которого у
облачного аккаунта не существует: там только ключ. Теперь при заданном
ключе недоступность локального адреса не считается отказом — аккаунт
проверяется по каталогу ollama.com и принимается с честной оговоркой,
что локальные модели недоступны. Проверено: 19 моделей каталога.
Без ключа поведение прежнее.
609 passed, ruff clean, релизный гейт 10/10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>