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

335 lines
36 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.

# 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`
```bash
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` и грепает подстроку токена; тест падает при совпадении.
---
### P0-2. После перезагрузки Hub все активные Links молча перестают работать
**Где:** `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`
```csharp
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_archive` `verify_manifest_signature` сверка хэша архива **с manifest**, а не с соседним файлом.
4. Отказ обновления при несовместимой паре версий ControlAgenthelper (`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.locale``system.packages` (versioned allowlist уже есть) → `system.swap``users.*``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 dest``rm -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.nft``table 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` безопасен, следующие модули нет).
### Тесты
9. Всё в одном проекте `ServerMonitorManager.Control.Tests` — включая `LinuxMetricsTests` (Agent), `MetricBufferTests` (Agent), `ProvisioningHelperTests` (Helper). Разделить: `Core.Tests`, `Control.Tests`, `Agent.Tests`, `Helper.Tests`, `Desktop.Tests`.
10. Нет тестов на: сериализацию контрактов (source-generated JSON + trimming — риск молчаливой потери полей при `PublishTrimmed=true`), отзыв сертификата → отказ в доступе, миграции SQLite v1→v8 на реальном старом файле, поведение при повреждённой БД.
11. Нет измеримых performance budget'ов (в roadmap отмечено). Задать: p95 heartbeat < 50 мс при 100 Node, старт Control < 2 с, память Agent < 60 МБ, размер БД на Node/сутки.
12. `HubLoadTests` (126 строк) гоняет 100 Node в одном процессе добавить soak на 24 ч в nightly workflow с проверкой отсутствия роста памяти и размера БД.
### Процесс и репозиторий
13. Отсутствуют: `CHANGELOG.md`, `SECURITY.md` (куда слать уязвимости обязательно для security-инструмента), `CONTRIBUTING.md`, `CODEOWNERS`, шаблоны issue/PR, `.github/dependabot.yml`.
14. Actions не запиннены по SHA; в `linux-release.yml` `permissions: contents: write` на весь workflow сузить до job'а публикации.
15. Нет SBOM (`dotnet CycloneDX`) и нет сканирования зависимостей для проекта, который ставит бинарь с root-правами, это ожидаемый минимум.
16. 12 переводов README рассинхронизированы с релиз-статусом (пункт открыт в roadmap Этап 6). Сделать один источник секцию статуса генерировать скриптом из тега, переводы проверять CI на наличие актуального номера версии.
17. Ветки `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 проверяет подпись
- [ ] Совместимость ControlAgenthelperDesktop проверяется перед 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 нет.