server-monitor-manager/agents/antigravity/inbox/from-smm-deliverables/Старые задачи/smm-antigravity-task-web-console-2026-08-17.md
Ochenstarik 23eb3f5233 chore(agents): разбор рабочих папок с диска на 2026-08-18
Задания, отчёты и патчи, лежавшие в C:\Users\Ochenstarik\projects и в
домашней папке, перенесены в agents/. Разложено по агентам там, где имя
файла позволяло определить автора; остальное — в _salvage-2026-08-18/
и разбирается вручную.

Патчи в notes/salvage-2026-08-18/ — незакоммиченная работа из брошенных
рабочих копий: она существовала только на диске.

Тяжёлое (релизные архивы, инсталляторы, наборы данных) в репозиторий не
попало: оно лежит рядом, в Agent_projects/_archive и Agent_projects/_data.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 14:19:54 +07:00

19 KiB
Raw Blame History

Antigravity: выдача кода регистрации узла из приложения — этап 1

Задание переписано 2026-08-17 после первой попытки. Тогда результатом стал файл с отчётом: ни ветки, ни PR, ни строчки кода в репозитории не появилось. Поэтому здесь сужен объём и указаны точные места в коде.

Что считается выполнением

Ссылка на PR в ochenstarik-ui/server-monitor-manager и ссылки на зелёные прогоны CI.

Документ с описанием проделанной работы результатом не является ни при каких обстоятельствах. Отчёт — это раздел в описании PR, а не отдельный файл. Если PR нет, задание не выполнено, независимо от того, что написано в отчёте.

Если задание невыполнимо или в нём ошибка — так и напишите, с указанием, что именно мешает. Это нормальный исход. Отчёт о несделанной работе как о сделанной — нет.

Репозиторий

  • https://github.com/ochenstarik-ui/server-monitor-manager
  • База: main @ 5dc4c39
  • Ветка: antigravity/node-enrollment-endpoint
  • main защищён: PR обязателен, обязательны статусы build-and-test и build, force-push запрещён
  • один PR — одна тема; описание PR заполняется после завершения CI

Задача этапа 1

Один HTTP-эндпоинт: оператор просит код регистрации нового узла, Control возвращает готовую строку SMMNODE2..., которую оператор вставляет в установщик на сервере.

Сегодня этот код можно получить только зайдя по ssh на Hub под root и выполнив ochenstarik-server-monitor-manager.sh node-code <имя>. Владелец хочет получать его из приложения, из которого и так ведётся наблюдение за парком.

Веб-интерфейс и вход по паролю — этапы 2 и 3, они описаны в конце и не начинаются, пока этап 1 не в main.

Что уже есть в коде

Большая часть механики существует, писать с нуля нужно немного.

В src/ServerMonitorManager.Control/Program.cs:

  • строка 575: var control = app.MapGroup("/api/v1/control").RequireAuthorization("Operator"); — группа эндпоинтов оператора. Новый эндпоинт добавляется сюда, роль тем самым обеспечена;
  • строка 144, ветка CLI token-create: выпуск токена уже реализован на C# — store.CreateEnrollmentTokenAsync(nodeId, TimeSpan.FromMinutes(10));
  • там же NodeIdValidator.IsValid(nodeId) — проверка имени узла на C# уже есть, изобретать не нужно;
  • строка 22: политика ограничения частоты "enrollment" уже определена и применяется к эндпоинтам регистрации.

В deploy/ochenstarik-server-monitor-manager.sh, функция create_node_code (строка 1224) — эталон формата. Строка собирается так:

SMMNODE2.<control_url>.<ca_pem>.<node_id>.<token>.<hub_endpoint>.<hub_public_key>.<node_address>.<mesh_network>

каждый сегмент — base64url без выравнивания. Источники сегментов:

Сегмент Откуда берётся
control_url файл control-public-url в каталоге конфигурации
ca_pem файл control-ca.crt
node_id из запроса
token CreateEnrollmentTokenAsync, срок 10 минут
hub_endpoint, hub_public_key, mesh_network mesh.env и wg/hub.pub
node_address reserve_node_address — резервирование адреса в 10.77.0.0/24

Таким образом дописать нужно: чтение файлов, резервирование адреса и сборку строки. Токен, валидация имени, авторизация по роли и ограничение частоты уже готовы.

Требования

  1. Формат совпадает побайтно. Код, выданный эндпоинтом, обязан приниматься установленным на сервере bootstrap без единого изменения в bash. Опубликованные релизы неизменяемы, менять формат нельзя.
  2. Адреса не выдаются дважды. Два запроса на разные node_id получают разные адреса. Как ведёт себя повторный запрос на существующий node_id — посмотрите в reserve_node_address и повторите это поведение; в отчёте напишите, какое оно.
  3. Срок жизни кода — 10 минут. Не увеличивать.
  4. Выдача попадает в журнал событий: кто выдал, какому node_id, когда.
  5. node_id не может привести к выполнению постороннего кода на Hub. Если решите вызывать bash-функцию через sudoers — аргументы фиксированные, подстановки произвольных строк нет. Если перенесёте резервирование адреса в C# — bash-версия и версия Control обязаны давать одинаковый результат на одинаковом входе.
  6. Роль Operator обязательна. Ни Agent, ни Automation доступа к выдаче не получают.

Тесты

В tests/ServerMonitorManager.Control.Tests:

  • запрос без сертификата Operator отклоняется;
  • сертификат роли Agent и роли Automation отклоняются;
  • корректный запрос возвращает строку, начинающуюся с SMMNODE2., с девятью сегментами;
  • сегменты декодируются из base64url и содержат ожидаемые значения: node_id совпадает с запрошенным, ca_pem — это сертификат, node_address лежит в 10.77.0.0/24;
  • два запроса на разные имена дают разные адреса;
  • недопустимое имя узла (пустое, с заглавными, длиннее 63 символов, с подчёркиванием) отклоняется до выпуска токена.

Тест на совпадение с bash-версией: сравнить структуру выданного кода с эталоном из create_node_code. Если полноценно вызвать bash в тестах невозможно — зафиксируйте эталонную строку как фикстуру и сверяйтесь с ней, а в отчёте напишите, что сравнение идёт с фикстурой, а не с живым скриптом.

Границы

Codex ведёт deploy/**, релизный пайплайн и проверку релиза; сейчас у него открыт PR #52. Не трогать: deploy/**, .github/workflows/linux-release.yml, .github/workflows/release-verification.yml, tests/release-verification/**, tests/bootstrap/**.

Ваша область: src/ServerMonitorManager.Control/**, src/ServerMonitorManager.Core/** при необходимости, tests/ServerMonitorManager.Control.Tests/**, документация по новому эндпоинту.

Критерий приёмки

  • PR открыт, CI зелёный, PR не смержен до зелёного;
  • эндпоинт доступен только роли Operator;
  • выданный код имеет формат SMMNODE2 с девятью сегментами и корректным содержимым;
  • два узла не получают один адрес;
  • срок жизни 10 минут, выдача записана в журнал;
  • имя узла проверяется до выпуска токена и не может привести к выполнению постороннего кода;
  • перечисленные тесты присутствуют и проходят.

Отчёт — в описании PR

Раздельно: что проверено локально, что в CI со ссылками на прогоны, что не проверялось и почему.


Доработка PR #53 — убрать дублирующий маршрут

Этап 1 выполнен, PR #53 открыт, CI зелёный, границы соблюдены. Одна правка до слияния.

Зарегистрированы два маршрута с одинаковым поведением:

  • control.MapPost("/agents/{nodeId}/enrollment-code", ...)Program.cs:606
  • control.MapPost("/nodes/{nodeId}/enrollment-code", ...)Program.cs:632

Обработчик скопирован целиком, второй раз. Просили один маршрут.

Удалить /nodes/{nodeId}/enrollment-code, оставить /agents/{nodeId}/enrollment-code. Основания:

  • все десять обращений в NodeEnrollmentCodeTests.cs идут на /agents/...; маршрут /nodes/... не покрыт ни одной проверкой — это непроверяемая публичная поверхность;
  • в группе /api/v1/control двадцать два маршрута, и /nodes/ среди них единственный. Соседи по смыслу — POST /agents/{nodeId}/reenroll и PUT /agents/{nodeId}/desired/preflight — уже используют /agents/{nodeId}/.

Если считаете, что /nodes/ семантически вернее, потому что узла как агента ещё не существует в момент выпуска кода, — доводы принимаются, но тогда переносите на него тесты и убирайте /agents/.... Два маршрута не остаются в любом случае.

Заодно, раз обработчик станет один: проверьте, что общий код не продублирован и в других местах PR.


Этап 2 — веб-консоль · активен с 2026-08-18

Этап 1 слит: main @ 7583b13, PR #53. Этап 2 начинается.

Ветка: antigravity/web-console. База — актуальный main; он движется быстро, Codex ведёт параллельно PR #57 и #58, поэтому зафиксируйте хеш базы в отчёте и обновляйте ветку перед слиянием.

Что уже доступно

POST /api/v1/control/agents/{nodeId}/enrollment-code, роль Operator, возвращает NodeEnrollmentCodeResponse: сам код SMMNODE2..., отпечаток CA и время истечения. Это ваш собственный эндпоинт из этапа 1 — читайте его контракт по коду, а не по этому заданию.

Важно про живой Hub. Эндпоинт в main, но на настоящем сервере он сейчас вернёт отказ доступа: Control работает под ochenstarik-smm-control, а каталог состояния mesh создаётся 0700 от root. Это чинит Codex в PR #57. На разработку консоли не влияет — тесты и локальный запуск подставляют свой путь, — но если будете проверять на Hub владельца и получите отказ, причина эта, а не ваш код.

Что сделать

Минимальная, но настоящая консоль оператора:

  • список узлов — данные отдаёт GET /api/v1/control/agents. Показать имя, состояние, время последнего heartbeat;
  • список LinksGET /api/v1/control/links;
  • кнопка «Добавить узел» — спрашивает имя, вызывает POST /api/v1/control/agents/{nodeId}/enrollment-code из этапа 1, показывает крупно код SMMNODE2... и отпечаток CA, с копированием в один клик;
  • рядом с кодом — срок его жизни и требование сверить отпечаток с тем, что покажет установщик на сервере. Оператор, который щёлкнет «да» не глядя, обесценивает всю проверку подписи, поэтому текст должен быть заметным, а не сноской.

Размещение

Отдельный проект, обслуживаемый Control, чтобы разворачиваемая единица оставалась одна. Если выберете иначе — обоснуйте в отчёте.

Учтите: до этапа 3 вход возможен только по клиентскому сертификату роли Operator. Значит консоль на этом этапе открывается браузером, которому сертификат подсунут вручную, и это нормально — так и задумано. Не пытайтесь обойти это ради удобства демонстрации; удобство — предмет этапа 3, и там оно ограничено условиями.

Чего не делать

  • не добавлять эндпоинты, которых не требует интерфейс;
  • не выносить в консоль операции, меняющие состояние парка, кроме выдачи кода: удаление узлов, правка Links, перезапуск служб — не сейчас;
  • не трогать deploy/** и релизные workflow.

Доработка PR #59 — покрыть тестом ветку встроенных ресурсов

Проверено 2026-08-18. Выполнено всё, и выполнено аккуратно: ассеты отдаются явными маршрутами в группе с RequireAuthorization("Operator"), а не через UseStaticFiles, поэтому закрыт и интерфейс, а не только API. Пять тестов на роли, настоящие эндпоинты, encodeURIComponent на имени узла, обратный отсчёт срока жизни, предупреждение о сверке отпечатка написано заметно и по делу.

Отдельно стоит отметить <EmbeddedResource Include="wwwroot\**" />: Control публикуется с PublishSingleFile=true, каталога wwwroot рядом с бинарём на сервере не будет, и вы это учли сами, без указания в задании. Именно на таких местах проект терял по релизу за раз.

Одна вещь осталась непроверенной, и это как раз та ветка, которая будет исполняться в бою.

GetWebConsoleAsset пробует два источника: файл по env.WebRootPath, затем ресурс сборки. В тестах хост поднимается в процессе, WebRootPath существует, поэтому срабатывает файловая ветка. На сервере из единого файла сработает встроенная, и её сейчас не проверяет ничто.

Добавить тест: поднять хост с несуществующим или пустым WebRootPath и убедиться, что /, /index.html, /style.css и /app.js отдаются с кодом 200, верным Content-Type и непустым содержимым — то есть достаются из ресурсов сборки. Заодно это зафиксирует соглашение об именах ресурсов: поиск идёт через EndsWith(fileName), и переименование каталога его молча сломает.

После этого PR вливается.

Критерий приёмки

  • консоль показывает узлы и Links из настоящих эндпоинтов Control, а не из заглушек;
  • ассеты отдаются из ресурсов сборки, когда каталога wwwroot на диске нет, — покрыто тестом;
  • кнопка выдаёт код, код копируется, отпечаток и срок жизни видны;
  • сборка консоли входит в CI и не ломает существующие прогоны;
  • есть проверка, что консоль недоступна без сертификата Operator;
  • PR, CI зелёный, PR не смержен до зелёного;
  • отчёт — в описании PR, отдельным файлом отчёт не является.

Этап 3. Вход по логину и паролю — только для тестов. Владелец просит его, чтобы не возиться с сертификатами при тестировании. Это второй, более слабый путь рядом с mTLS, и он не должен незаметно стать основным. Условия: выключен по умолчанию, включается явным параметром вида Authentication:PasswordLogin:EnabledForTesting; при включении предупреждение в логе при каждом старте; пароль хранится подбор-стойким хешем (Argon2id или bcrypt); ограничение частоты и задержка после неудач; ограниченная по времени сессия; роль — Operator и не выше; существующая проверка mTLS не ослабляется ни в одной ветке кода, при обоих включённых механизмах сертификат имеет приоритет.