Задания, отчёты и патчи, лежавшие в 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>
18 KiB
Задача 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 и делался:
ochenstarik-smm-emergency firewall-restore— правила стёрты, ни один Node не уходил в offline, реконсиляция не запускается. В Desktop: «Расхождение: нет». Фактически трафик заблокирован.docs/linux-bootstrap.md:111продолжает обещать переприменение, которого нет.- Ручной
systemctl restart ochenstarik-smm-firewall. - Перезапуск Hub с простоем < 90 с (перезапуск сервиса, быстрый reboot VM,
update-control). - 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 разных ошибок вместо одной причины.
deploy/ochenstarik-smm-policy-apply: отличать «таблица/цепочка недоступна» от «правило не найдено» — отдельный exit code (например 79) и стабильный маркер в stderr. Сохранить fail-closed поведение: неизвестная ошибкаnftпо-прежнему не должна читаться как «правил нет».LinkPolicyApplier: типизированноеMeshFirewallUnavailableExceptionпо этому коду.- Сервис реконсиляции: при таком исключении — прервать проход, один раз опубликовать
mesh.firewall-unavailable, перевести затронутые политики вPartialс единым кодом ошибки, повторить с backoff. Никаких массовых попыток применения в несуществующую таблицу. - Desktop: один баннер «Mesh firewall не загружен» вместо N строк с ошибками.
B2-3. Маркер внеочередной реконсиляции
ochenstarik-smm-emergency firewall-restoreиmesh-enableсоздают/var/lib/ochenstarik-server-monitor-manager/mesh/reconcile-requested(root,0600).- Фоновый сервис на каждом тике проверяет маркер, при наличии выполняет внеочередной проход и удаляет маркер только после успешного завершения.
- Контрактная проверка в
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-blobkeyless либо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.shcSMM_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, а не как закрытые.