server-monitor-manager/agents/_salvage-2026-08-18/smm-deliverables/Старые задачи/smm-improvement-plan-2026-07-30.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

36 KiB
Raw Permalink Blame History

Server Monitor Manager — полная инструкция по доработке

Ревизия: main @ 2d28b8d («Execute confirmed timezone provisioning safely», 30.07.2026). Публичный релиз: v0.1.0-alpha.5 (17.07.2026) — отстаёт от main на 3 коммита (bootstrap + provisioning execution ещё не выпущены). Открытых issue и PR нет, вся работа идёт напрямую в main через merged PR.


0. Как читать документ

Приоритет Значение
P0 Блокирует alpha-тест на реальных серверах: утечка секрета, потеря доступа, ложное состояние безопасности. Чинить до следующего тега.
P1 Блокирует beta: расхождение с собственным ТЗ, DoS, невоспроизводимая поставка.
P2 Долг качества: архитектура, тесты, DX, локализация.

Каждый пункт: что не так → где → что сделать → как проверить.


1. Что фактически готово

Проект существенно более зрелый, чем типичный alpha: mTLS control plane с разделением ролей, идемпотентность, аудит, versioned SQLite (user_version до 8), bounded offline-буфер агента, подписанные ECDSA execution grants, restricted root helper через Unix-сокет, CI на Ubuntu VM + Debian systemd-контейнерах, 100-node нагрузочный тест.

Реально работает:

  • Control API: enroll (node/device/automation), heartbeat, links CRUD + TTL, event stream (NDJSON), provisioning jobs, preflight facts / desired / drift, backup-create/restore CLI.
  • Bootstrap: install/update/rollback/uninstall Control и Agent, mesh-init, node-code (SMMNODE1/2), peer-add (SMMPEER1), emergency-команда.
  • Provisioning: preflight (read-only) и system.base-install — но исполняется только смена timezone.
  • Desktop: 4 страницы, SSH-мониторинг, история метрик, графики, redacted diagnostics.

Заявлено, но не реализовано (расхождение README/docs ↔ код):

Заявление Реальность
docs/installer-contract.md §2 «роль Monitor»: пользователь ochenstarik-monitor, root-owned forced command В deploy/ochenstarik-server-monitor-manager.sh нет действия install-monitor, нет самого forced-command скрипта. SSH-мониторинг из Desktop настраивается вручную и нигде не описан.
docs/linux-bootstrap.md:82 «После перезапуска firewall разрешающие правила должны быть повторно применены Control» В Control нет кода переприменения Active-Link'ов.
ТЗ §14: endpoints /api/v1/nodes/{id}/capabilities, /configuration, /users, /vpn-profiles Отсутствуют.
ТЗ §7.1 «подписанный version manifest» Manifest не подписан, содержит только hash bootstrap-скрипта, без версий Control/Agent/helper.
README: «system.base-install… factual verification» Verification только для timezone; IsTimezoneOnly() отвергает всё остальное.

2. P0 — критические

P0-1. Enrollment-токен утекает через argv в /proc/*/cmdline

Где: deploy/ochenstarik-server-monitor-manager.sh:564-570

runuser -u "$AGENT_USER" -- env \
    ... \
    "SMM_EnrollToken=$ENROLL_TOKEN" \
    "$LIB_DIR/agent/ochenstarik-smm-agent"

Аргументы env — это командная строка процесса. /proc/<pid>/cmdline читается любым локальным пользователем (в отличие от environ, который защищён). Токен живёт 10 минут и даёт право получить сертификат Agent для этого Node.

Что сделать:

  1. Ввести в AgentOptions поддержку SMM_EnrollTokenFile (путь к файлу, режим 0600, владелец — agent).
  2. В bootstrap: install -m 0600 -o "$AGENT_USER" -g "$AGENT_USER" /dev/null "$token_file", записать токен через printf, передать путь, shred -u после enrollment.
  3. Альтернатива без нового API: runuser -u agent -- env SMM_EnrollTokenFile=/proc/self/fd/3 ... 3< <(printf '%s' "$ENROLL_TOKEN").
  4. AgentClient.EnrollAsync — читать файл, CryptographicOperations.ZeroMemory после использования, File.Delete.

Проверка: тест в tests/bootstrap/ — во время enrollment фоновый цикл читает /proc/*/cmdline и грепает подстроку токена; тест падает при совпадении.


Где: deploy/ochenstarik-smm-firewall.service:9-10, deploy/ochenstarik-server-monitor-manager.sh:263-281 (write_mesh_firewall), src/ServerMonitorManager.Control/Program.cs (нет startup-реконсиляции).

Юнит firewall при каждом старте делает nft delete table inet ochenstarik_smm, затем загружает mesh.nft с пустой цепочкой links. Все accept-правила, добавленные link-connect, исчезают. Control при старте не переприменяет Active-политики — реконсиляция (LinkService.ReconcileDisabledLinksForNodeAsync) переприменяет только disabled.

Итог: в БД и в Desktop Link показан Active, фактически трафик заблокирован. Это прямое нарушение критерия приёмки №12 собственного ТЗ («после reboot factual state соответствует desired»). Направление отказа безопасное (fail-closed), но состояние в UI ложное — а ложное «зелёное» состояние в security-инструменте хуже, чем красное.

Что сделать:

  1. Добавить в policy-helper действие link-list, возвращающее фактические правила (парсинг nft -j list chain) как строки source:target:proto:port.
  2. Новый LinkStartupReconciliationService : IHostedService в Control:
    • при старте и далее раз в LinkExpirationPollSeconds × N получать факт-список,
    • для каждой Link с DesiredState=Active, отсутствующей в факте → ApplyConnectAsync, событие link.reapplied,
    • для каждого факт-правила без записи в БД → ApplyDisconnectAsync, событие link.orphan-removed + audit,
    • публиковать ActualState=Partial до успешного восстановления.
  3. Добавить ActualState колонку в UI Links и явный бейдж «desired ≠ factual».
  4. ochenstarik-smm-emergency firewall-restore — после восстановления deny-by-default дёргать systemctl reload ochenstarik-smm-control либо сбрасывать маркер, чтобы Control переприменил.

Проверка: интеграционный тест с фейковым ILinkPolicyApplier (стартовое факт-состояние пустое → ожидаем N вызовов connect); + в tests/acceptance/three-server-mesh.sh при SMM_ACCEPT_REBOOT=1 после reboot Hub проверять nc -z через Link, а не только состояние в API.


P0-3. Приватный SSH-ключ пишется на диск в открытом виде каждые 30 секунд

Где: src/ServerMonitorManager.Desktop/SshMonitorService.cs:187-210 (MaterializePrivateKeyAsync), вызывается из RunRestrictedCommandAsync:122 на каждый опрос метрик.

Ключ хранится под DPAPI, но для запуска ssh.exe расшифровывается и пишется в ApplicationData.TemporaryFolder без ACL-ограничения, удаляется в finally. При падении процесса, kill'е или гонке файл остаётся. Частота: каждые 30 с × число серверов.

Что сделать (в порядке предпочтения):

  1. Лучшее: отказаться от внешнего ssh.exe для мониторинга — перейти на in-process SSH-клиент (SSH.NET), ключ живёт только в памяти. Интерактивный терминал оставить на ssh.exe/wt.exe.
  2. Минимум, если п.1 откладывается:
    • создавать файл один раз за сессию приложения, а не на каждый запрос;
    • FileOptions.DeleteOnClose + удержание FileStream на время жизни сессии;
    • явный DACL только для текущего пользователя (FileSecurity, SetAccessControl);
    • при старте приложения — чистка orphan-файлов server-monitor-manager-ed25519-* в TemporaryFolder;
    • ProcessExit/AppDomain.UnhandledException хук на удаление.

Проверка: unit-тест на чистку orphan-файлов; ручной тест — kill процесса в момент опроса, перезапуск, проверка что TemporaryFolder пуст.


P0-4. StrictHostKeyChecking=accept-new без подтверждения fingerprint

Где: src/ServerMonitorManager.Desktop/SshMonitorService.cs:132

Первое подключение к любому серверу принимает любой host key без показа пользователю. Это ровно тот MITM-вектор, который остальная часть проекта тщательно закрывает (Control CA fingerprint подтверждается вручную, mTLS с CustomRootTrust). ТЗ §10 п.2 и §13 требуют фиксации host key.

Что сделать:

  1. В ServerProfileData добавить HostKeyFingerprint (SHA-256) и HostKeyAlgorithm.
  2. Первое подключение: ssh-keyscan → показать fingerprint в диалоге → после подтверждения записать в профиль и в known_hosts.
  3. Далее всегда StrictHostKeyChecking=yes.
  4. При смене ключа — блокирующее предупреждение с явным действием «принять новый ключ» + запись в аудит Desktop.
  5. Интерактивный терминал (OpenInteractiveTerminal:159-163) сейчас вообще не передаёт UserKnownHostsFile/StrictHostKeyChecking — использует системный known_hosts. Привести к тому же контракту.

P0-5. Root helper вешается насмерть от одного молчащего клиента

Где: src/ServerMonitorManager.Provisioning.Helper/ProvisioningHelperServer.cs:37-53 и 185-203

var connection = await listener.AcceptAsync(cancellationToken);
try { await HandleAsync(connection, cancellationToken); }   // последовательно!

HandleAsync обрабатывается синхронно в цикле accept, а ReadRequestAsync читает по одному байту без таймаута. Любой процесс с доступом к сокету (группа ochenstarik-smm-agent) открывает соединение, ничего не пишет — и root-helper перестаёт обслуживать кого-либо навсегда. Провиженинг встаёт, Requires= в юните Agent не помогает (процесс жив).

Что сделать:

  1. Обрабатывать соединение в отдельной задаче с ограничением параллелизма (SemaphoreSlim(4)).
  2. Таймаут на всё соединение: CancellationTokenSource.CreateLinkedTokenSource(ct) + CancelAfter(TimeSpan.FromSeconds(30)).
  3. Читать буфером (ArrayPool<byte>), а не по байту — сейчас 16 КБ = 16384 syscall'а.
  4. Проверять SO_PEERCRED: socket.GetRawSocketOption(SOL_SOCKET, SO_PEERCRED, …), сверять uid с ожидаемым uid пользователя Agent (передавать через env юнита). Сейчас достаточно членства в группе.
  5. Rate limit: не более N запросов в минуту с одного uid, лог отклонений.

Проверка: тест ProvisioningHelperTests — открыть 2 соединения, первое молчит, второе шлёт валидный preflight и должно получить ответ за < 2 с.


P0-6. Поставка не подписана: компрометация GitHub-аккаунта = RCE на всех Node

Где: .github/workflows/linux-release.yml:33-65, deploy/ochenstarik-server-monitor-manager.sh:128-146 (verify_archive)

Bootstrap проверяет ARCHIVE.sha256, лежащий рядом с архивом в том же релизе. Manifest (server-monitor-manager-bootstrap-manifest.json) не подписан и не содержит хэшей архивов Control/Agent/helper — только хэш самого bootstrap. Кто может залить релиз, может залить и hash. update-agent/update-control принимают любой такой архив.

Собственный контракт (installer-contract.md §1, ТЗ §7.1) требует signed compatibility manifest — не выполнено (в roadmap отмечено как открытое, но это P0 для любой публичной поставки).

Что сделать:

  1. Подписывать manifest — cosign sign-blob (keyless/OIDC) либо minisign. Публичный ключ вшить в bootstrap-скрипт константой и в Desktop.
  2. Manifest должен содержать: версию тега, sha256 каждого артефакта (bootstrap, оба tar.gz, MSIX), версии Control/Agent/helper/Desktop, поддерживаемые runtime, минимальную совместимую версию Control.
  3. verify_archiveverify_manifest_signature → сверка хэша архива с manifest, а не с соседним файлом.
  4. Отказ обновления при несовместимой паре версий Control↔Agent↔helper (installer-contract.md §6 это уже требует).
  5. Запиннить все GitHub Actions по commit SHA (actions/checkout@<sha>), включить dependabot для actions.

3. P1 — важные

P1-1. Роль Monitor не реализована в bootstrap

Флагманская функция (SSH-мониторинг из Desktop) не имеет серверной части в поставке: нет install-monitor, нет forced-command скрипта, нет создания ochenstarik-monitor, нет установки публичного ключа из Desktop.

Сделать: добавить в bootstrap действие install-monitor PUBLIC_KEY:

  • создать ochenstarik-monitor (nologin, без пароля, без sudo);
  • /usr/local/libexec/ochenstarik-smm-metrics — root-owned 0755, выдающий ровно снимок из installer-contract.md §7 (PROTOCOL=1, HOSTNAME=…) и read-only mesh status;
  • authorized_keys c command="/usr/local/libexec/ochenstarik-smm-metrics",no-pty,no-agent-forwarding,no-port-forwarding,no-X11-forwarding,restrict;
  • идемпотентность при повторном запуске, отдельный uninstall-monitor.
  • Формат снимка вынести в общий тест-фикстуру, чтобы SshMonitorService и скрипт не разъезжались (сейчас парсер в SshMonitorService.QueryAsync:72-106 знает ключи, которых никакой скрипт в репозитории не производит).

P1-2. Приватный ключ Control CA доступен процессу Control

create_control_certificates (:451) экспортирует CA в PKCS#12 с пустым паролем, 0640 root:ochenstarik-smm-control. Любой RCE в Control = выпуск произвольных Agent/Operator/Automation сертификатов + подпись execution grant для любого Node.

Сделать:

  • разделить ключи: CA-ключ для выпуска сертификатов и отдельный ключ подписи execution grant (helper пиннит второй);
  • защитить PFX паролем, подавать через systemd LoadCredential= (ControlOptions.CertificateAuthorityPassword уже есть — использовать);
  • задокументировать процедуру ротации CA и re-enrollment всего флота;
  • сократить срок сертификатов клиентов с 1 года (CertificateAuthority.cs:55) до 3090 дней + автопродление Agent'ом за 1/3 срока до истечения.

P1-3. Event stream без keepalive, без resume, без лимита подписчиков

Program.cs:922-937 + ControlEventBroker.cs. Канал DropOldest(256) — при медленном клиенте события теряются молча. Реконнект Desktop (ControlClientService.ListenAsync:181-184, фиксированные 5 с без backoff и джиттера) начинает с текущего момента: всё, что произошло во время разрыва, потеряно, а полного refresh на реконнекте нет.

Сделать: таблица control_events с retention; GET /api/v1/control/events?since=<sequence>; heartbeat-кадр раз в 15 с ({"type":"ping"}) для детекта мёртвого TCP; лимит подписок на identity; экспоненциальный backoff с джиттером на клиенте; после реконнекта — принудительный refresh inventory и links.

P1-4. Rate limiting только на enrollment

Program.cs:19-31 — политика "enrollment". Heartbeat, links, provisioning, events — без лимитов. Скомпрометированный Agent-сертификат = неограниченная запись метрик и рост БД (retention сработает поздно).

Сделать: политики на группу agents (по NameIdentifier), на control (мутации), и лимит одновременных event-подписок. Плюс Kestrel.Limits.MaxConcurrentConnections.

P1-5. Провиженинг реализован на ~5% от ТЗ

system.base-install исполняет только timezone; IsTimezoneOnly (TimezoneProvisioningExecutor.cs:229-238) жёстко требует VmSwappiness == 60, пустые пакеты, RebootPolicy == "never". Этапы 912 ТЗ (базовая настройка, пользователи, firewall/SSH-миграция, Xray) — нули.

Сделать: вынести общий каркас исполнителя, прежде чем писать второй модуль. Сейчас timezone-логика (проверка grant → consumption record → backup → mutate → verify → rollback) вшита в один класс на 320 строк; второй модуль скопирует её целиком.

IProvisioningModule
  string ActionType { get; }
  string ModuleHash { get; }
  ValidationResult Validate(JsonElement parameters);
  Plan BuildPlan(parameters);           // без мутаций
  ExecutionResult Execute(plan, grant); // backup → mutate → verify → rollback
  FactualState Observe();

Порядок модулей (по риску, от низкого к высокому): system.timezone (готов) → system.localesystem.packages (versioned allowlist уже есть) → system.swapusers.*firewall.*ssh.migrate (двухфазный) → xray.*.

P1-6. Grant consumption-записи копятся без ограничения

TimezoneProvisioningExecutor.cs:79-95 пишет grant-<sha>.consumed.json в rollback-каталог навсегда. За год активного провиженинга — десятки тысяч файлов в одном каталоге, замедление CreateNew.

Сделать: чистка записей старше 24 ч (grant живёт 2 мин) при каждом старте helper'а; тест на идемпотентность повторного grant после чистки (должен по-прежнему отклоняться в пределах TTL — хранить не файл, а компактный журнал nonce с TTL).

P1-7. HttpClient создаётся на каждый запрос

ControlClientService.cs:254-281 и AgentClient.CreateHttpClient:390-416 — новый HttpClientHandler + TLS handshake на каждый вызов, исчерпание сокетов в TIME_WAIT при 30-секундном опросе.

Сделать: один SocketsHttpHandler на сессию (PooledConnectionLifetime = 5 min), переиспользование HttpClient. Сертификат сессии держать в X509Certificate2 с явным Dispose при смене identity.

P1-8. Атомарность установки нарушена

install_tree_atomic:200-213: rm -rf destination; mv staging destination. Между этими командами каталога с бинарями не существует — падение/kill/OOM оставляет систему без Control.

Сделать: mv dest dest.old.$$mv staging destrm -rf dest.old.$$; при неудаче второго шага — вернуть .old.

P1-9. restore_backup распаковывает tar в / без валидации

:711 tar -C / -xzf "$archive" — в отличие от verify_archive, без проверки путей и без sha256. Каталог 0700 root, риск ограничен, но rollback — именно тот путь, который выполняется в аварийной ситуации.

Сделать: записывать .sha256 рядом с каждым backup'ом, проверять перед распаковкой; прогонять тот же allowlist путей.

P1-10. Нет фильтрации входящего mesh-трафика на Node

Node получает AllowedIPs = 10.77.0.0/24 (:407) и не имеет никакой nftables-политики. Вся изоляция держится на forward-цепочке Hub. Hub (или тот, кто его скомпрометировал) имеет неограниченный доступ ко всем портам всех Node поверх smm0.

Сделать: ставить на Node input-цепочку iifname "smm0" с deny-by-default и разрешением только на порты, фигурирующие в Link'ах, где этот Node — target. Разрешающие правила Node применяет сам Agent по данным /api/v1/automation/links-подобного эндпоинта (нужен /api/v1/agents/links/inbound). Это же закрывает единственную точку отказа изоляции.

P1-11. IPv6 в mesh не определён

mesh.nfttable inet, но политика описана только для IPv4 (ip saddr/daddr в policy-helper). Если на Node включён IPv6 внутри smm0 (не включён, но AllowedIPs не запрещает), правил нет. Явно задать: mesh — IPv4-only, IPv6 в smm0 запрещён отдельным правилом drop.


4. P2 — качество, архитектура, процесс

Код

  1. ControlStore.cs — 1576 строк, «божественный объект»: схема + миграции + токены + enrollment + heartbeat + links + audit + idempotency + provisioning-делегаты. Разделить по агрегатам: ControlSchema (миграции), IdentityStore, MetricStore, LinkStore, AuditStore, IdempotencyStore; provisioning уже частично вынесен. Тесты уже намекают на границы (ControlStoreTests 809 строк).
  2. MainPage.xaml.cs — 1124 строки code-behind без MVVM и DI. Вынести в ViewModel c INotifyPropertyChanged, сервисы — через IServiceProvider в App.xaml.cs. Сейчас логика Desktop покрыта одним PowerShell-скриптом на 39 строк (tests/windows/Test-DesktopContracts.ps1) — фактически не покрыта.
  3. Хардкод русских строк в бизнес-логике: SshMonitorService.cs:117,155,216,220,226, ControlClientService.cs:42,51,60,73,83,89,119,297,318. При 12 переводах README само приложение не локализовано. Ввести .resw + x:Uid, коды ошибок вместо текста в исключениях (текст — на уровне UI).
  4. Валидаторы живут в Program.cs (NodeIdValidator, LinkPolicyValidator, ProvisioningJobValidator, …, строки 1005-1136) и дублируются в bash (validate_node_id, validate_port) и в helper. Три независимые реализации одних правил. Вынести в Core, а для bash — генерировать regexp из одного источника либо покрыть контрактным тестом (test-bootstrap-contract.sh уже есть — расширить: каждый валидатор проверяется одинаковым набором кейсов на обеих сторонах).
  5. Нет Directory.Build.props: не заданы Nullable, TreatWarningsAsErrors, EnableNETAnalyzers, AnalysisLevel, InvariantGlobalization, детерминированная сборка. Добавить.
  6. Нет Directory.Packages.props (Central Package Management) и нет lock-файлов (RestorePackagesWithLockFile) — сборки невоспроизводимы, что противоречит требованию pinned-поставки.
  7. LinkPolicyApplier вызывает sudo per-operation (:48); при массовом переприменении (P0-2) это N процессов. Добавить batch-режим helper'а: link-apply - со списком правил на stdin, одна транзакция nft -f.
  8. ProvisioningProcessRunner.Run (:329-343) — Task.WaitAll на потоке; при чтении больших выводов возможен deadlock-паттерн. Переписать на async, добавить лимит размера stdout/stderr (сейчас без ограничения — timedatectl безопасен, следующие модули нет).

Тесты

  1. Всё в одном проекте ServerMonitorManager.Control.Tests — включая LinuxMetricsTests (Agent), MetricBufferTests (Agent), ProvisioningHelperTests (Helper). Разделить: Core.Tests, Control.Tests, Agent.Tests, Helper.Tests, Desktop.Tests.
  2. Нет тестов на: сериализацию контрактов (source-generated JSON + trimming — риск молчаливой потери полей при PublishTrimmed=true), отзыв сертификата → отказ в доступе, миграции SQLite v1→v8 на реальном старом файле, поведение при повреждённой БД.
  3. Нет измеримых performance budget'ов (в roadmap отмечено). Задать: p95 heartbeat < 50 мс при 100 Node, старт Control < 2 с, память Agent < 60 МБ, размер БД на Node/сутки.
  4. HubLoadTests (126 строк) гоняет 100 Node в одном процессе — добавить soak на 24 ч в nightly workflow с проверкой отсутствия роста памяти и размера БД.

Процесс и репозиторий

  1. Отсутствуют: CHANGELOG.md, SECURITY.md (куда слать уязвимости — обязательно для security-инструмента), CONTRIBUTING.md, CODEOWNERS, шаблоны issue/PR, .github/dependabot.yml.
  2. Actions не запиннены по SHA; в linux-release.yml permissions: contents: write на весь workflow — сузить до job'а публикации.
  3. Нет SBOM (dotnet CycloneDX) и нет сканирования зависимостей — для проекта, который ставит бинарь с root-правами, это ожидаемый минимум.
  4. 12 переводов README рассинхронизированы с релиз-статусом (пункт открыт в roadmap Этап 6). Сделать один источник — секцию статуса генерировать скриптом из тега, переводы проверять CI на наличие актуального номера версии.
  5. Ветки agent/remove-lightweight-server-references и codex/ttl-backup-acceptance слиты, но не удалены — почистить.

5. План работ по спринтам

Спринт 1 — «Безопасно тестировать на живых серверах» (P0)

  • P0-1 токен через файл/fd + тест на /proc/*/cmdline
  • P0-5 helper: параллелизм, таймауты, буферное чтение, SO_PEERCRED
  • P0-3 SSH-ключ: чистка orphan, DACL, одноразовая материализация за сессию
  • P0-4 host key fingerprint в профиле + StrictHostKeyChecking=yes
  • Критерий выхода: v0.1.0-alpha.6, ни один секрет не наблюдаем локальным непривилегированным пользователем; helper выдерживает молчащего клиента.

Спринт 2 — «Состояние в UI = состояние в системе» (P0-2, P1-3, P1-10)

  • link-list в policy-helper, LinkStartupReconciliationService
  • ActualState/drift-бейдж в Desktop
  • durable event log + ?since= + keepalive + backoff
  • input-политика на Node
  • Критерий выхода: сценарий из three-server-acceptance.md с SMM_ACCEPT_REBOOT=1 проходит с фактической проверкой связности (nc -z) после reboot Hub и Node.

Спринт 3 — «Поставка, которой можно доверять» (P0-6, P1-2, P1-8, P1-9)

  • подписанный compatibility manifest + вшитый публичный ключ
  • проверка совместимости версий при update
  • разделение CA-ключа и grant-ключа, пароль через LoadCredential
  • атомарная замена дерева, checksum для backup'ов
  • пиннинг actions, dependabot, SBOM, SECURITY.md
  • Критерий выхода: v0.2.0-beta.1; подмена архива в релизе отвергается bootstrap'ом; тест на это в CI.

Спринт 4 — «Каркас провиженинга» (P1-5, P2-1, P2-4)

  • IProvisioningModule + перенос timezone на него без изменения поведения
  • versioned JSON schema per action, единый источник валидаторов в Core
  • модули system.locale, system.packages, system.swap с backup/verify/rollback
  • распил ControlStore
  • Критерий выхода: Этап 9 roadmap закрыт наполовину; добавление нового модуля = один файл + схема + тест.

Спринт 5 — «Роль Monitor и Desktop» (P1-1, P2-2, P2-3)

  • install-monitor + forced-command скрипт + контрактный тест формата снимка
  • MVVM + DI + unit-тесты Desktop
  • локализация .resw, коды ошибок вместо текста
  • Критерий выхода: чистый сервер настраивается под мониторинг одной командой; README-инструкция соответствует коду.

Спринт 6 — «SSH-миграция и firewall» (Этап 10 ТЗ)

Двухфазная миграция порта — самая опасная операция во всём проекте (потеря доступа). Делать только после спринтов 14, обязательно с VM-матрицей и тестом «новый порт не поднялся → 22 остался открыт».


6. Чек-лист готовности к публичной beta

  • Ни один секрет (токен, ключ, subscription URL) не наблюдаем в argv, логах, diagnostics, SQLite — покрыто автотестом
  • Все артефакты релиза подписаны, bootstrap проверяет подпись
  • Совместимость Control↔Agent↔helper↔Desktop проверяется перед update и перед выполнением job
  • После reboot Hub и любого Node factual state == desired state (проверено связностью, не только API)
  • Отзыв сертификата немедленно закрывает доступ — тест
  • Каждая мутирующая операция: idempotency + audit + verification + rollback — тест на каждую
  • Физическая приёмка three-server-acceptance.md с SMM_ACCEPT_RESTORE=1 SMM_ACCEPT_REBOOT=1
  • Debian VM с настоящим reboot (сейчас только systemd-контейнеры)
  • Доверенная подпись Windows MSIX
  • Performance budget зафиксированы и проверяются в nightly
  • SECURITY.md, CHANGELOG.md, SBOM
  • Переводы README синхронизированы с релиз-статусом

7. Чего делать не стоит

  • Не начинать Xray (Этапы 1112) до закрытия P0/P1. Xray добавляет системный kill switch и policy routing — при текущем разрыве desired/factual (P0-2) это гарантированная потеря доступа к серверу.
  • Не расширять system.base-install копированием TimezoneProvisioningExecutor. Сначала каркас (P1-5), иначе шесть копий логики backup/verify/rollback.
  • Не добавлять macOS/Linux/мобильные клиенты (Этап 13) до стабилизации контрактов Core — сейчас они меняются каждым PR.
  • Не отключать PublishTrimmed ради удобства, но и не доверять ему без тестов сериализации (P2-10): source-generated JSON контекст есть, но контрактных тестов на round-trip нет.