server-monitor-manager/agents/_salvage-2026-08-18/smm-deliverables/Старые задачи/smm-task-4-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 Permalink Blame History

Приёмка Task 3 / B-2 и Задача 4

Базовая ревизия: main @ b11c277 (PR #12, squash merge, 21 файл, 1113/172). Предшествующие документы: smm-improvement-plan-2026-07-30.md, smm-task-2-2026-07-31.md, smm-task2-blockB-review-2026-08-03.md, smm-task-3-2026-08-03.md.


0. Приёмка B-2: принято

Проверял по origin/main, а не по отчёту. Все семь контрольных сумм сходятся, CI 14/14 на PR #12, PATCH_VERIFICATION.txt подтверждает применимость патча к чистой базе.

Требование задачи 3 Проверка в main @ b11c277
B2-1 фоновый сервис, старт + интервал Program.cs:63AddHostedService<LinkReconciliationBackgroundService>; do { RunOnceAsync } while (timer.WaitForNextTickAsync) — первый проход немедленный
Control__LinkReconciliationSeconds ControlOptions.cs:27 (=300), валидация Program.cs:49 (30…3600), appsettings.json:13, control.env в bootstrap
Проход по всем действующим политикам ReconcileAllAsync через ListEffectiveLinksAsync, не по одному Node
Порядок блокировок сохранён sorted node locks → per-Link gate, новых классов не введено
link.reapplied / link.orphan-removed LinkService.cs:309
Единая логика сходимости ConvergeAsync — используется create/disable/reconnect/full/TTL/retry. Три копии сведены к одной
B2-2 «таблицы нет» ≠ «правил ноль» helper: exit 79 + точный маркер mesh.firewall-unavailable в stderr, LC_ALL=C; типизированное исключение; проход прерывается; агрегированное событие; bounded backoff; событие восстановления mesh.firewall-available
B2-3 маркер внеочередной реконсиляции firewall-restore и mesh-enable публикуют маркер атомарно под общим root-flock с UUID-поколением; helper потребляет только точное поколение
B2-4 тесты LinkReconciliationTests.cs (338 строк), контрактные проверки в bootstrap-тесте, интеграционный тест последовательности вызовов helper
Acceptance: firewall-restore без перезапуска Node новый шаг [8/12] с retry-циклом по фактическому статусу и реальной связности
M1 GetLinkAsync, L1 CR/LF оба исправлены REPORT.md метки M1 и L1 перепутаны местами — сами правки на месте)
M2, M4, M5 явно перенесены в B-3 с обоснованием в docs/roadmap.md — это и требовалось по DoD
docs/linux-bootstrap.md приведён в соответствие с реализацией

Отдельно отмечу два места, где сделано лучше, чем было в задании:

  • Я просил просто маркер. Реализовано поколение-UUID под общим flock с потреблением ровно прочитанного поколения — то есть закрыта гонка «пока шёл проход, пришёл новый запрос», о которой в задании не было ни слова.
  • Маркер обходит только регулярный интервал, но не обходит backoff недоступного firewall. Это правильное разделение, и оно покрыто тестом.

Независимый review в этот раз проводился против полного текста задачи, а не только дифа — рекомендация из прошлого документа принята и сработала: его находки (маркер против throttle, слепое удаление чужого поколения, выход acceptance по первому несовпадению, баннер при нуле SSH-профилей, локализованный stderr nft) — это ровно тот класс дефектов, который в прошлый раз никто не искал.

Статус блока: merged / verified, physical acceptance pending. Формулировка в отчёте корректна, B6 не объявлен закрытым. Это правильно и так и должно остаться до прогона на реальной тройке.


1. Находки по B-2 (не блокирующие merge, в работу)

N1 — MEDIUM. Незавершаемый маркер даёт полный проход каждые 30 секунд бесконечно

LinkReconciliationBackgroundService.RunOnceAsync:

if (requestGeneration is null && _nextRegularAt is not null && now < _nextRegularAt) return null;
...
else { _nextRegularAt = now + interval;
       if (requestGeneration is not null && result.Failed == 0)
           await applier.CompleteReconciliationAsync(requestGeneration, ct); }

Маркер потребляется только при Failed == 0, а наличие маркера полностью снимает регулярный throttle. _backoffUntil взводится только для FirewallUnavailable, обычные отказы под него не попадают.

Следствие: одна политика, которая падает стабильно и не связана с firewall — например Node удалён из nodes.tsv, и lookup_node_ip возвращает 78, — навсегда удерживает маркер. Полный проход начинает выполняться на каждом poll-тике (min(interval, 30) = 30 с) без ограничения, каждый проход — до двух sudo+nft на каждую подходящую политику. При полусотне Link это сотня привилегированных вызовов каждые полминуты на неопределённый срок.

Сделать: применить ограниченный backoff и к marker-пути. Например: счётчик неуспешных prompt-проходов, после 34 попыток маркер перестаёт снимать регулярный throttle, в журнал пишется одно предупреждение с идентификаторами упавших политик. Тест: политика, падающая всегда, при висящем маркере не должна вызывать более N проходов за интервал.

N2 — MEDIUM. Orphan-правило для полностью сошедшейся Disabled-политики не обнаруживается

private static bool IsEligibleForFullReconciliation(LinkPolicy link)
    => link.DesiredState == "Active"
        || (link.DesiredState == "Disabled" && link.ActualState != "Disabled");

Политика в состоянии Disabled/Disabled из полного прохода исключена — то есть если accept-правило с её комментарием окажется в цепочке, никто этого не заметит. А это ровно инвариант kill switch.

Это моя недоработка в постановке, а не отклонение исполнителя — фильтр в задаче 3 сформулирован мной дословно так же. Отмечаю, чтобы не потерялось.

Реалистичные пути появления такого правила: восстановление сохранённого ruleset nft -f поверх, частично применённый backup, обрыв между успешным link-connect и записью в БД, отказ на середине link.orphan-removed.

Правильное решение — развернуть направление прохода. Сейчас идём БД → факт, по одной политике, двумя вызовами helper на каждую. Нужно наоборот: один вызов link-list, возвращающий все правила цепочки с комментарием smm:*, и сверка множеств:

  • правило есть, политики в БД нет либо DesiredState=Disabled → удалить, link.orphan-removed;
  • политика DesiredState=Active, правила нет → применить, link.reapplied;
  • совпало → ничего.

Это одновременно закрывает N2, снимает зависимость от IsEligibleForFullReconciliation, и превращает стоимость прохода из 2N привилегированных вызовов в один. link-list был в исходной постановке Block B (пункт B1) и до сих пор не реализован — сейчас есть только пер-линковый link-status.

Связано с M4: пока нет retention для завершённых Disabled-политик, обход БД→факт растёт неограниченно. Обход факт→БД от этого не зависит.

N3 — LOW. На Hub без mesh-init предупреждение каждые 30 секунд

install_control создаёт $STATE_DIR и $STATE_DIR/backups, но не $STATE_DIR/mesh; каталог появляется только в mesh_init / reserve_node_address. Helper в reconcile_status делает exec 9>"$RECONCILE_LOCK" в несуществующем каталоге — под set -e это отказ. В RunOnceAsync он ловится и пишется LogWarning, то есть в journal попадает предупреждение каждые 30 секунд до первого mesh-init.

Сделать: helper возвращает complete, если каталог mesh отсутствует (Mesh ещё не инициализирован — запросов быть не может), либо создаёт каталог при первом обращении.

N4 — LOW. Отсутствующий бинарь nft неотличим от отсутствующей таблицы

inspect_firewall берёт объединённый вывод 2>&1 и классифицирует по подстроке No such file or directory. Если /usr/sbin/nft не установлен, эту же строку выдаёт сам bash — и ситуация классифицируется как mesh.firewall-unavailable (79). Поведение fail-closed и мутаций не будет, так что дефект не опасный, но оператор увидит «Mesh firewall не загружен» вместо «nftables не установлен».

Сделать: явная проверка [[ -x /usr/sbin/nft ]] с отдельным сообщением до вызова.

N5 — LOW. Форматирование в three-server-mesh.sh

В блоке probe_factual_status / expect_factual_status перемешаны отступы 4 и 2 пробела, закрывающая } вынесена внутрь тела, определение expect_factual_status() { смещено на два пробела. bash -n проходит, поведение верное, но остальной файл написан с отступом в 2 пробела. Похоже на тот же артефакт инструмента, что в прошлый раз дал (char)13. Переформатировать.

N6 — nit. REPORT.md меняет местами метки M1 и L1

Написано «M1 (CR/LF compaction) и L1 (direct Link lookup)»; в задаче M1 — это GetLinkAsync, L1 — CR/LF. Обе правки сделаны, перепутаны только подписи.


2. Задача 4

Блок B-3 (первым)

Переносится из roadmap плюс находки выше.

ID Что Приоритет
N2 + B1 link-list в policy-helper и разворот прохода реконсиляции на факт → БД; удаление orphan-правил независимо от состояния в БД; один привилегированный вызов на проход вместо 2N высокий
N1 Ограниченный backoff для marker-пути; тест на политику, падающую всегда высокий
M2 Развести LinkReconciliationResult на Examined / Converged / Failed; сейчас «1 реконсилирован» при нуле мутаций средний
M4 Фильтр по умолчанию на Links-странице («действующие + расхождения», с переключателем) и/или retention завершённых Disabled-политик в ControlMaintenance средний
M5 Типизировать «Node зарезервирован, но не активирован в mesh» — сейчас lookup_node_ip даёт отказ применения и Link уходит в Failed на каждом проходе средний
N3 Helper возвращает complete при отсутствующем каталоге mesh низкий
N4 Явная проверка наличия nft с отдельным сообщением низкий
N5 Переформатировать three-server-mesh.sh низкий

Критерий выхода B-3: проход реконсиляции обнаруживает и снимает accept-правило, которому не соответствует ни одна действующая политика, включая случай Disabled/Disabled; стабильно падающая политика не вызывает более одного прохода за интервал; Links-страница остаётся читаемой при сотнях исторических политик.

Блок C — доверенная поставка

Без изменений относительно smm-task-2-2026-07-31.md, раздел 3, и smm-task-3-2026-08-03.md, раздел 3:

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

Physical acceptance — внешний блокер

Не выполнялся ни разу за три задачи. Требуется:

HUB_SSH_HOST, HUB_SSH_USER, SOURCE_SSH_HOST, SOURCE_SSH_USER,
HOME_WG_IP, SECOND_WG_IP, SSH_IDENTITY_FILE

и прогон

SMM_ACCEPT_RESTORE=1 SMM_ACCEPT_REBOOT=1 tests/acceptance/three-server-mesh.sh

Пока он не выполнен, B6 и вся Этап 3 roadmap остаются открытыми, независимо от того, сколько раз CI будет зелёным. Сейчас harness готов и включает шаг firewall-restore без перезапуска Node — то есть сценарий, ради которого делались Block B и B-2, впервые проверяем по-настоящему. Это единственный пункт списка, который не может закрыть исполнитель.


3. Порядок

  1. B-3, приоритет по таблице: N2+B1 и N1 в одном PR (они оба про сервис реконсиляции), M2/M4/M5 вторым, N3/N4/N5 попутно.
  2. Physical acceptance — как только выданы topology inputs. По результату закрыть B6 и соответствующие пункты Этапа 3 и 5 roadmap.
  3. Блок C.

4. Процесс — работает, оставить

Практика «независимый review получает полный текст задачи, а не только диф» подтвердилась: пять находок этого цикла относятся к классу, который в прошлый раз не искали вовсе. Сохранить и для B-3, и для Блока C — для Блока C особенно, там корректность дифа и корректность модели угроз расходятся ещё сильнее.

Формулировка статуса merged / verified, physical acceptance pending вместо «выполнено» — тоже оставить.