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

184 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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;
- **список Links** — `GET /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 не ослабляется ни в одной ветке кода, при обоих включённых механизмах сертификат имеет приоритет.