server-monitor-manager/agents/hermes/notes/salvage-2026-08-18/hermes-desktop-attachments/desktop-attachments/smm-task-3-2026-08-03.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

18 KiB
Raw Blame History

Задача 3 — закрыть P0-2 по существу и перейти к доверенной поставке

Базовая ревизия: main @ 89ef2fd (PR #11, Block B merged). Предшествующие документы: smm-improvement-plan-2026-07-30.md, smm-task-2-2026-07-31.md, smm-task2-blockB-review-2026-08-03.md.


0. Результат перепроверки пакета Block B

Процессная часть — принято. SHA256SUMS сходится по всем семи файлам, репорт репрезентативен, CI 14/14 SUCCESS на 1580ec7 (включая Debian 12/13 x64+arm64 systemd restart и Ubuntu 22.04/24.04 native VM), PR #11 смержен как 89ef2fd, временный verifier удалён. Замечание по контрольным суммам из прошлого ревью закрыто.

Содержательная часть — не принято. Проверял по origin/main, а не по отчёту. Диф совпадает с тем, что я ревьюил (551 против 550 additions), и главное замечание в него не вошло:

Проверка в main @ 89ef2fd Результат
Хостед-сервисы в Program.cs:61-62 только LinkExpirationBackgroundService и ControlMaintenanceBackgroundService
Вызовы ReconcileLinksForNodeAsync ровно один — Program.cs:356, обработчик heartbeat
Control__LinkReconciliationSeconds отсутствует
mesh.firewall-missing / маркер от firewall-restore отсутствуют
(char)13 в CompactError LinkService.cs:339, на месте
ListLinksAsync().SingleOrDefault(...) LinkService.cs:32, LinkService.cs:297

Условие запуска реконсиляции не изменилось:

var reconnectThreshold = TimeSpan.FromSeconds(Math.Max(60, nextHeartbeatSeconds * 3)); // 90 с
var requiresReconciliation = previousStatus != "Online"
                             || previousLastSeenAt is null
                             || now - previousLastSeenAt >= reconnectThreshold;

Следовательно, в main по-прежнему открыты сценарии, ради которых Block B и делался:

  1. ochenstarik-smm-emergency firewall-restore — правила стёрты, ни один Node не уходил в offline, реконсиляция не запускается. В Desktop: «Расхождение: нет». Фактически трафик заблокирован. docs/linux-bootstrap.md:111 продолжает обещать переприменение, которого нет.
  2. Ручной systemctl restart ochenstarik-smm-firewall.
  3. Перезапуск Hub с простоем < 90 с (перезапуск сервиса, быстрый reboot VM, update-control).
  4. Link, оба участника которого не переподключались.

Критерий выхода B6 не выполнен. Block B остаётся открытым, несмотря на merge. Раздел 1 этой задачи — его остаток, а не новая функциональность.

Отдельно: physical acceptance не выполнялся ни разу за две задачи, потому что не выданы HUB_SSH_HOST, HUB_SSH_USER, SOURCE_SSH_HOST, SOURCE_SSH_USER, HOME_WG_IP, SECOND_WG_IP, SSH_IDENTITY_FILE. Это внешний блокер, который снимается только вашим решением. Пока он не снят, ни один критерий, формулируемый через фактическую связность, закрыть нельзя — CI это не заменяет.


1. Блок B-2 — остаток Block B (обязательный)

B2-1. Фоновая реконсиляция, не привязанная к переподключению Node

Новый LinkReconciliationBackgroundService : BackgroundService, регистрируется третьим в Program.cs.

  • Настройка Control__LinkReconciliationSeconds в ControlOptions, валидация в цепочке .Validate(...) наравне с прочими (диапазон 30…3600, по умолчанию 300), значение в appsettings.json.
  • Один проход при старте — после store.InitializeAsync(), до начала обслуживания запросов не обязателен, но первый тик должен быть немедленным, а не через интервал.
  • Проход идёт по всем действующим политикам (DesiredState = Active, либо DesiredState = Disabled при ActualState != "Disabled"), а не по Link конкретного Node.
  • Порядок блокировок сохранить существующий: node-локи (отсортированные) → per-Link gate. Никаких новых классов блокировок.
  • Актор — system:reconcile.
  • События: при фактическом восстановлении публиковать link.reapplied, при удалении правила без записи в БД — link.orphan-removed. Существующие link.reconciling / link.active / link.disabled / link.failed / link.partial сохранить.
  • Не чаще одного полного прохода за интервал независимо от внешних триггеров.

Обязательный сопутствующий рефакторинг. Логика «прочитать текущее состояние → сравнить факт с desired → применить → проверить → сохранить» сейчас существует в трёх экземплярах: CreateAsync, ReconcileLinksForNodeAsync, ConvergeDisabledCoreAsync. Четвёртая копия в фоновом сервисе недопустима. Свести к одному методу вида

private async Task<LinkPolicy> ConvergeAsync(
    LinkPolicy link, bool expectedConnected, string actor, CancellationToken ct)

и вызывать его из всех четырёх мест.

B2-2. «Таблицы нет» ≠ «правил ноль»

Сейчас при отсутствии таблицы link-status завершается кодом 78, исключение ловится поштучно, и каждая политика по очереди получает Failed/Partial с текстом ошибки helper'а. Оператор видит N разных ошибок вместо одной причины.

  1. deploy/ochenstarik-smm-policy-apply: отличать «таблица/цепочка недоступна» от «правило не найдено» — отдельный exit code (например 79) и стабильный маркер в stderr. Сохранить fail-closed поведение: неизвестная ошибка nft по-прежнему не должна читаться как «правил нет».
  2. LinkPolicyApplier: типизированное MeshFirewallUnavailableException по этому коду.
  3. Сервис реконсиляции: при таком исключении — прервать проход, один раз опубликовать mesh.firewall-unavailable, перевести затронутые политики в Partial с единым кодом ошибки, повторить с backoff. Никаких массовых попыток применения в несуществующую таблицу.
  4. Desktop: один баннер «Mesh firewall не загружен» вместо N строк с ошибками.

B2-3. Маркер внеочередной реконсиляции

  1. ochenstarik-smm-emergency firewall-restore и mesh-enable создают /var/lib/ochenstarik-server-monitor-manager/mesh/reconcile-requested (root, 0600).
  2. Фоновый сервис на каждом тике проверяет маркер, при наличии выполняет внеочередной проход и удаляет маркер только после успешного завершения.
  3. Контрактная проверка в tests/bootstrap/test-bootstrap-contract.sh — как для остальных инвариантов emergency-команды.

B2-4. Тесты

  • Unit: все правила стёрты (fake applier: IsConnected == false для всех), ни одного heartbeat → фоновый проход выполняет N connect и приводит ActualState к Active. Это тот самый тест, который ловит текущий пробел; без него блок не принимается.
  • Unit: правило существует, DesiredState = Disabled → disconnect + link.orphan-removed.
  • Unit: firewall unavailable → ноль мутаций, событие опубликовано один раз, затронутые в Partial.
  • Unit: два прохода подряд без изменений → ноль вызовов helper во втором.
  • Integration в стиле LinkPolicyApplierIntegrationTests: удалить маркер connected, выполнить проход, сверить точную последовательность вызовов helper.
  • Acceptance (tests/acceptance/three-server-mesh.sh): новый шаг — на Hub выполнить emergency firewall-restore (или nft delete table + systemctl start ochenstarik-smm-firewall) без перезапуска Node, выждать интервал реконсиляции, затем expect_reachable "$HOME_WG_IP", expect_blocked "$SECOND_WG_IP" и expect_factual_status для обоих.

B2-5. Критерий приёмки блока

Правила nftables стёрты при полностью живых Node → связность по всем DesiredState=Active восстанавливается автоматически не позднее одного интервала реконсиляции; Disabled остаются заблокированными; правил без записи в БД не остаётся; расхождение видно в Desktop до момента устранения. Подтверждается unit-тестом и прогоном acceptance-скрипта на реальной тройке.


2. Блок B-3 — техдолг из ревью (в том же PR или следующем, до закрытия Block B)

ID Что Где
M1 Опубликовать GetLinkAsync(string id, CancellationToken) — приватный ReadLinkAsync(id) уже есть — и заменить им поиск по id полным перечислением. Сейчас O(N) чтений и десериализаций под захваченной блокировкой, что ударит по нагрузочному тесту Этапа 5. ControlStore.cs:1460; LinkService.cs:32, 297; цикл в ReconcileLinksForNodeAsync
M2 LinkReconciliationResult.Reconciled теперь означает «рассмотрено», а не «приведено в порядок» (тест ожидает (1, 0) при нуле вызовов helper). Развести на Examined / Converged / Failed. LinkService.cs
M4 Фильтр на Links-странице снят, retention для таблицы links нет (чистятся только metric_samples, idempotency, audit, токены) — страница превращается в журнал. Нужен фильтр по умолчанию «действующие + расхождения» с переключателем либо retention для завершённых Disabled-политик. MainPage.xaml.cs, ControlMaintenance.cs
M5 Проба статуса для зарезервированного, но не активированного в mesh Node падает через lookup_node_ip и перебрасывает Link в Failed на каждом проходе. Отличать «Node ещё не активирован» от «helper сломан». ochenstarik-smm-policy-apply, LinkService
L1 Split([(char)13, '\n'], …) вернуть к Split(['\r', '\n'], …) — это то же самое, но нечитаемо; похоже на артефакт инструмента. LinkService.cs:339

M1 и L1 — дешёвые, делать сразу. M4 влияет на пригодность UI и требует решения о retention — вынести отдельным PR можно, но до объявления Block B закрытым.


3. Блок C — доверенная поставка (после закрытия B-2)

Полное описание — smm-task-2-2026-07-31.md, раздел 3. Кратко:

  • C1. Manifest v2: хэши всех артефактов (bootstrap, оба tar.gz, MSIX), версии Control/Agent/helper/Desktop, минимальные совместимые версии, helper_protocol.
  • C2. Подпись (cosign sign-blob keyless либо minisign); публичный ключ/issuer вшит константой в bootstrap и Desktop, не скачивается вместе с релизом. Отсутствие инструмента проверки — отказ, не предупреждение.
  • C3. verify_archive сверяет хэш с manifest, а не с соседним .sha256; старый путь только под явным SMM_ALLOW_UNSIGNED=1. Новое действие verify-manifest. Отказ update-* при несовместимой паре версий. PROGRAM_VERSION подставляется из тега при упаковке вместо 0.2.0-dev.
  • C4. Пиннинг GitHub Actions по commit SHA (сейчас плавающие @v6, @v5, @v2), dependabot.yml для github-actions и nuget, сужение permissions: contents: write до job'а публикации, SBOM в артефакты релиза.
  • C5. Негативные тесты в CI: изменённый байт в архиве, подменённый хэш в manifest без переподписи, manifest без подписи — все три отвергаются.

4. Порядок и Definition of Done

Порядок: B2-1 → B2-2 → B2-3 → B2-4 (в одном PR, это единая функция) → B-3 (M1, L1 в том же PR; M2, M4, M5 — отдельным) → physical acceptance → Блок C.

DoD задачи 3:

  • Фоновая реконсиляция работает при старте и по интервалу, не завися от переподключения Node
  • Unit-тест «правила стёрты, heartbeat не приходил → связность восстановлена» проходит
  • Недоступный mesh firewall даёт одно событие и одно понятное состояние, а не N ошибок
  • emergency firewall-restore приводит к внеочередной реконсиляции
  • Логика сходимости существует в одном экземпляре, а не в четырёх
  • docs/linux-bootstrap.md:111 соответствует реализации
  • M1, L1 исправлены; по M2, M4, M5 принято решение (сделать либо явно перенести с обоснованием)
  • Physical acceptance three-server-mesh.sh c SMM_ACCEPT_RESTORE=1 SMM_ACCEPT_REBOOT=1 выполнен — иначе B6 остаётся открытым
  • Далее: Блок C по разделу 3

5. Процесс

Две вещи, которые стоит изменить в способе работы, — обе подтвердились на Block B.

Независимому review отдавать текст задачи, а не только диф. Оба review Block B были корректны по существу написанного кода и оба пропустили, что половина требований блока не реализована. Первый проверял «безопасно ли коммитить этот диф», второй — «закрыты ли две HIGH». Полноту относительно постановки не проверял никто, и блок ушёл в main с открытым основным критерием.

Критерий выхода блока не равен merge. Block B помечен выполненным на основании зелёного CI и APPROVE, тогда как его собственный критерий B6 сформулирован через фактическую связность и не проверялся. Пока physical acceptance заблокирован отсутствием topology inputs, блоки, чьи критерии формулируются через факт, следует помечать как merged, acceptance pending, а не как закрытые.