Harden RF node control and agent updates

This commit is contained in:
LittleSam129
2026-09-30 11:42:19 +03:00
parent a94f2db4ee
commit 0d6fff224e
34 changed files with 14010 additions and 0 deletions

View File

@@ -0,0 +1,223 @@
# RF001 bootstrap 2026.09.29.5
Дата: 29 сентября 2026 года.
## Production fix
Launcher теперь проверяет два допустимых варианта process identity:
- `DIRECT`: health PID совпадает с `Popen.pid`;
- `WINDOWS_VENV_WRAPPER`: health PID принадлежит единственному непосредственному потомку проверенного venv wrapper.
В обоих вариантах проверяются живой процесс, PID, PPID, creation time, полный путь executable, точная командная строка, release ID, agent version, одноразовый launch nonce и `state=RUNNING`. Перед принятием health process identity проверяется второй раз. Failure cleanup завершает только доказанную lineage и не выбирает процессы по имени.
Health schema и `rf001_rx_http_agent.py` не изменялись.
Локальные проверки: `102 passed`. Python compile и PowerShell parse прошли. Ruff и Pyright отсутствуют, поэтому не запускались.
## Windows integration
Изолированный запуск на RX использовал настоящий Windows venv, исправленный production launcher, canonical release A и настоящий agent health writer. Scheduled Task и legacy agent не изменялись.
- `Popen.pid`: 11000;
- health PID: 13248;
- topology: wrapper 11000 -> direct child 13248;
- production `start_release()`: PASS;
- strict target identity: PASS;
- cleanup: PASS;
- orphan: 0;
- HTTP task polling: false;
- hardware accessed: false;
- RF TX: 0.
Evidence:
```text
C:\Users\Professional\Downloads\SDR_Rover_Diagnostics\launcher_identity_fix_20260929T135321Z\evidence\diagnostic_summary.json
```
## Bootstrap
- ID: `rf001-bootstrap-2026.09.29.5`;
- SHA256: `4833cd22f3f1bd41ae1a847c66f258ccf9d4c6fb4eec5feb2d36e688324c6554`;
- размер: 1 506 140 байт;
- HMAC: PASS;
- manifest-файлы: 17, расхождений 0;
- release A: 10 файлов из 10 побайтно совпадают с `.4`, включая `READY.json`;
- signed `agent_release_bootstrap_id`: `rf001-bootstrap-2026.09.29.4`;
- `.1`, `.2`, `.3` и `.4` не изменялись;
- SOURCE/RX SHA256: MATCH.
## Единственная попытка установки
`INSTALL_RX_AGENT.cmd` был запущен один раз. Результат: `exit code 1`, fail-closed.
Фактическая ошибка:
```text
Unexpected file in the bootstrap package: install.stderr.txt
```
Причина доказана. Оркестратор заранее создал `install.stdout.txt` и `install.stderr.txt` внутри распакованного bootstrap-каталога, чтобы перенаправить вывод `cmd.exe`. Installer затем проверил строгий allowlist всех файлов каталога и отверг созданный оркестратором `install.stderr.txt` как посторонний файл. Ошибка возникла до копирования launcher, изменения Scheduled Task и остановки legacy agent.
Это не ошибка HMAC, manifest, immutable release A или process identity fix. Bootstrap `.5` остался подписанным и прошёл preflight. Повторная установка запрещена текущим STOP-условием. `.6` не создавался.
Для следующей отдельно разрешённой попытки вывод установщика должен сохраняться вне распакованного bootstrap-каталога, а запуск должен использовать новый чистый каталог распаковки. Текущий загрязнённый каталог не следует использовать повторно.
## Безопасное состояние после отказа
- Scheduled Task: `Running`, прежнее legacy action;
- active agent: `2026.09.25.1`;
- MAIN heartbeat: свежий;
- unfinished tasks: 0;
- installed launcher SHA256: `fb99882351ee96802d1edd9de947634f2a559e849399c8153a1290688030ed79`, прежний `.4` launcher;
- production agent topology: PID 7980 -> PID 17568;
- `hackrf_transfer`: 0;
- RX capture: 0;
- B update: NOT_RUN;
- staircase search: NOT_RUN;
- new RX: 0;
- new RF TX: 0.
## Отдельно разрешённая попытка 2
После нового разрешения тот же неизменный ZIP `.5` повторно проверен на MAIN и
RX. SHA256 в обоих случаях:
```text
4833cd22f3f1bd41ae1a847c66f258ccf9d4c6fb4eec5feb2d36e688324c6554
```
Первая распаковка с `install.stdout.txt` и `install.stderr.txt` сохранена без
изменений: 21 файл до и после второй попытки. Для второй попытки создан новый
каталог:
```text
C:\Users\Professional\Downloads\SDR_Rover_Bootstrap\rf001-bootstrap-2026.09.29.5_attempt2_clean
```
До запуска установщика получены следующие результаты:
- HMAC: PASS;
- manifest: 17/17;
- файлов в пакете с двумя файлами аутентификации: 19/19;
- unexpected: 0;
- missing: 0;
- mismatch: 0;
- release A: 10/10;
- `INSTALL_RX_AGENT.cmd`: присутствует;
- canonical release A: 10/10, расхождений 0;
- active task: отсутствует;
- unfinished tasks: 0;
- unfinished hardware tasks: 0;
- `hackrf_transfer`: 0;
- RX capture process: 0.
Все runtime-файлы второй попытки сохранены вне bootstrap-каталога:
```text
C:\Users\Professional\AppData\Local\SDR_Rover_RX_Agent\install_logs\bootstrap_2026.09.29.5_attempt2_20260929T150351603Z
```
Установщик запущен ровно один раз из чистого каталога с явным рабочим
каталогом. Результат: `exit code 0`, stderr пуст. Дерево пакета сразу после
старта и после завершения побайтно не изменилось; timestamps также совпали.
Карантин из 11 файлов и первая загрязнённая распаковка не изменились.
После установки Scheduled Task запускает versioned launcher. Release A
`2026.09.29.1` сообщил свежий MAIN heartbeat, launcher status перешёл в
`RUNNING / TARGET_HEALTHY`, active task отсутствовал. Legacy-процессов: 0.
Наблюдаемая цепочка Windows состояла из launcher wrapper, его Python-потомка,
agent wrapper и единственного agent child с одним launch nonce. Это одна
production lineage, а не два экземпляра агента.
## Обновление A -> B
Пакет `rf001-agent-2026.09.29.2.zip` проверен до постановки задачи:
- SHA256: `225a032a927b672343f4ccf7a0ef0662f462e1ec45782c0437ba50ab73bac203`;
- release ID: `rf001-agent-2026.09.29.2`;
- agent version: `2026.09.29.2`;
- внутренний manifest: 9 файлов, расхождений 0;
- зарегистрированный HMAC: PASS.
Обновление выполнено только через подписанный HTTP-канал:
```text
UPDATE_20260929T151002Z_8B82A943
```
Задача завершилась со статусом `COMPLETE`, self-test вернул 0. Launcher
переключился на B, active release и свежий MAIN heartbeat показывают
`2026.09.29.2`, launcher status: `RUNNING / TARGET_HEALTHY`. Rollback не
использовался. SCP/SSH-установка B не выполнялась.
## Полный поиск лестницы
После подтверждения B поставлена одна задача:
```text
STAIRCASE_20260929T151048Z_EF3AA9DE
```
Итог:
- session: `RF001A_20260925T125829Z_227EADB4`;
- coverage: 0...2 400 000 000 комплексных отсчётов, 300,0 с;
- RX SHA256: подтверждён;
- reference SHA256: подтверждён;
- result: `NOT_DETECTED`;
- лучший кандидат: sample 474 212 320, 59,27654 с;
- CFO: -3,0 Гц;
- candidate score: 0,009649134828942169;
- contrast to background p99: 4,052909297771804 дБ, ниже порога;
- spectral contrast: 3,6405801238037765 дБ;
- step pattern: FAIL;
- наблюдаемые ступени: +2,879031, -2,205885 и +15,541302 дБ при ожидаемых
+6,0 дБ;
- maximum absolute step error: 9,54130207648743 дБ;
- analysis time: 263,3271906 с;
- peak additional memory: 496 549 888 байт;
- peak process memory: 536 199 168 байт;
- hardware accessed: false;
- RF TX: 0.
Причины вердикта: корреляция ниже порога, контраст пика к фону ниже порога,
структура ступеней или спектральное подтверждение не прошли. Пороги и алгоритм
не менялись, повтор задачи не выполнялся.
Отчёт на RX:
```text
C:\Users\Professional\Documents\SDR_Rover_Runtime\rx_capture\RF001A_20260925T125829Z_227EADB4\staircase_search_report.json
SHA256=b34ad21ae70a4290a0394a3ee3317bdd560e43dbe266aa3d7e1ce1c967c11dd7
```
Результат принят MAIN и сохранён в
`.local/rf001_http_server_state.json` под указанным task ID.
## Исправление MAIN-оркестрации
Подписанный `.5` не изменялся. На MAIN добавлена отдельная утилита
`tools/rf001_bootstrap_install_orchestrator.py`. Она до запуска проверяет HMAC,
manifest, SHA256 и точный allowlist, запрещает каталог доказательств внутри
package tree, перенаправляет stdout/stderr только наружу и сравнивает снимки
пакета сразу после старта и после завершения.
Регрессии в `tests/test_rf001_bootstrap_install_orchestrator.py` проверяют:
- чистый пакет до запуска;
- внешние stdout/stderr;
- неизменность package tree;
- успешный точный allowlist;
- fail-closed для пакета с посторонним файлом;
- отказ от evidence-каталога внутри пакета.
Итоговый RF001-набор: `122 passed`. Полный набор репозитория: `1296 passed`,
`4 failed`, `2 skipped`. Четыре сбоя находятся вне изменённого контура: два
старых теста `capture_process_runner.ps1` получили `sha256=null`, один тест
требует отсутствующий `data/raw/lab064/tx_metadata.json`, ещё один требует
недоступный Numba backend. Новая утилита компилируется; её отдельные регрессии:
`4 passed`.
Новый bootstrap `.6` для этого исправления не требуется.

View File

@@ -0,0 +1,310 @@
# HTTP-канал управления RF-001
Текущее состояние: **HTTP-КАНАЛ РАБОТАЕТ; ВЕРСИОННЫЙ МЕХАНИЗМ ОБНОВЛЕНИЯ
И НЕБЛОКИРУЮЩИЙ `STAIRCASE_SEARCH` ПРОВЕРЕНЫ БЕЗ НОВОГО ДОСТУПА К РАДИО**.
Канал заменяет Git как транспорт между работающими компьютерами. Git остаётся
для исходного кода и истории. Старые watcher-ы сохранены как резервный вариант.
## Граница этапа 1
Реализованы:
- JSON-сервер на основном ПК;
- исходящее подключение RX-агента с ноутбука к основному ПК;
- подпись каждого запроса HMAC-SHA256;
- защита от повторного использования запроса по времени и nonce;
- ограничение адреса RX-агента на сервере;
- фиксированные задачи `INVENTORY`, `OFFLINE_VERIFY`, `RX_CAPTURE_START`,
`RX_CAPTURE_STATUS`, `CAPTURE_DECODE`, `CAPTURE_DIAGNOSTICS` и
`STAIRCASE_SEARCH` и `UPDATE_AGENT`;
- чтение `hostname`, `whoami`, состояния Git и `hackrf_info`;
- сохранение результата на основном ПК;
- обязательное поле `rf_tx = 0` во всех сообщениях.
На RX-ноутбуке 23 сентября 2026 года автономная проверка завершилась так:
- ветка `video-development`;
- HEAD `a94f2db4ee8377201a97263166cb09431df5f5d6`;
- 100 CRC-корректных пакетов из 100;
- 100 точных пакетов из 100;
- `hardware_accessed = false`;
- ошибок нет;
- `rf_tx = 0`.
В сеансе `RF001A_20260925T125829Z_227EADB4` уже были выполнены:
- один конечный RX-захват с фиксированным профилем;
- проверка READY по фактическому PID, обнаруженному CLKIN и растущему файлу;
- чтение итогового состояния захвата;
- потоковое декодирование принятого IQ;
- полный потоковый поиск эталонной лестницы без обращения к SDR.
Новые захваты и передачи не входят в текущее состояние и требуют отдельного
явного разрешения. Завершённый поиск лестницы сохраняет исходный результат
`NOT_DETECTED`; последующая диагностика должна записываться отдельно и не
подменять этот результат.
Не входят в управляющий канал:
- запуск `hackrf_transfer` в режиме TX;
- команда или сетевой метод RF TX;
- изменение firmware, CPLD, USB-драйвера или Zadig;
- commit и push.
Для сервера и RX-агента подготовлен пользовательский автозапуск через
Планировщик заданий Windows. Службы Windows и права администратора для этого не
нужны. Сервер и агент работают постоянно после входа соответствующего
пользователя в Windows. Агент не содержит произвольной shell-команды.
## Схема
```text
ОСНОВНОЙ ПК 192.168.255.185:8765
HTTP JSON server
очередь только фиксированных типов заданий
^
| исходящее соединение с подписью запроса
|
RX-НОУТБУК 192.168.255.56
polling agent
hostname / whoami / Git / hackrf_info / offline 100/100
RF TX = 0
```
Сервер не раздаёт каталоги и произвольные пути. Он может выдать только заранее
зарегистрированный архив агента с точным `release_id` через ограниченный маршрут
обновлений. Произвольной shell-команды в протоколе нет. Неизвестный тип задания
и любое значение `rf_tx`, отличное от нуля, отклоняются.
## Файлы
- `tools/rf001_http_control.py`: сервер, локальная очередь и клиент управления;
- `tools/rf001_rx_http_agent.py`: RX-агент с фиксированной инвентаризацией;
- `tools/rf001_offline_decode.py`: фиксированный программный `legacy`-декодер;
- `tools/rf001_staircase_search.py`: потоковый поиск эталонной лестницы без
обращения к SDR;
- `tools/rf001_staircase_job.py`: фиксированный дочерний процесс длительного
поиска и сохранение результата до сетевого подтверждения;
- `tools/rf001_agent_update.py`: строгая проверка состава и распаковка
версионного архива;
- `tools/rf001_agent_launcher.py`: стабильный запускатель, переключение версии
и локальный откат при неудачном запуске;
- `tools/rf001_build_agent_packages.py`: воспроизводимая сборка выпусков и
первоначального пакета без включения секрета;
- `tools/rf001_rx_capture.py`: жёсткие RX-параметры и проверки READY/COMPLETE;
- `tools/rf001_capture_runner.py`: один конечный фоновый RX-процесс и журнал;
- `.local/rf001_http_token.txt`: общий секрет вне Git;
- `.local/rf001_http_server_state.json`: локальное состояние вне Git.
## Подготовка общего секрета
На основном ПК из корня проекта один раз выполнить:
```powershell
python -B tools\rf001_http_control.py init-token
```
Команда не перезаписывает существующий секрет. Файл
`.local\rf001_http_token.txt` надо скопировать на RX-ноутбук по доверенному
каналу и не добавлять в Git. Сервер и агент должны читать одинаковое содержимое.
## Запуск сервера на основном ПК
```powershell
python -B tools\rf001_http_control.py serve `
--bind 192.168.255.185 `
--port 8765 `
--allowed-agent-ip 192.168.255.56
```
Ожидаемый стартовый вывод:
```text
SERVER = http://192.168.255.185:8765
ALLOWED AGENT IP = 192.168.255.56
TASK KINDS = INVENTORY, OFFLINE_VERIFY, RX_CAPTURE_START, RX_CAPTURE_STATUS, CAPTURE_DECODE, CAPTURE_DIAGNOSTICS, STAIRCASE_SEARCH, UPDATE_AGENT
RF TX = 0
```
Действующее правило Windows Firewall должно оставаться ограниченным программой
Python, TCP-портом `8765`, локальным адресом `192.168.255.185`, удалённым
адресом `192.168.255.56` и беспроводным интерфейсом.
## Постановка задания на основном ПК
```powershell
python -B tools\rf001_http_control.py enqueue-inventory --agent-id user-pc
```
Это локальная операция. Сервер принимает её только от собственного адреса и с
правильной подписью. Задание не содержит произвольной команды.
## Автозапуск агента на RX-ноутбуке
Первоначальный пакет содержит `INSTALL_RX_AGENT.cmd`. После распаковки его надо
запустить один раз. Установщик проверяет HMAC-манифест существующим секретом,
размеры и SHA256 всех файлов, сохраняет прежнюю установку и XML задания, а
затем меняет только действие существующего пользовательского задания
`SDR Rover RX Agent`. Пользователь, триггеры, параметры и уровень прав задания
сохраняются. Ожидаемый итог:
```text
BOOTSTRAP_INSTALL=COMPLETE
TASK_STATE=Running
AGENT_VERSION=2026.09.29.1
ARBITRARY_SHELL=DISABLED
RF_TX=0
```
После этого ручной запуск на ноутбуке для штатных обновлений не требуется.
Запускатель хранит выпуски в отдельных каталогах, проверяет запуск по PID,
версии и одноразовой метке и при сбое возвращается к предыдущему локальному
выпуску. Очередь MAIN, IQ, эталоны, результаты, конфигурация и секрет находятся
вне каталога конкретной версии.
## Модель доверия обновлений
Манифест каждого выпуска подписывается `HMAC-SHA256` из стандартной библиотеки
Python тем же общим секретом, которым уже подписываются HTTP-запросы. Подпись
охватывает отдельный домен `RF001-UPDATE-MANIFEST-V1`, `release_id`, версию
агента, совместимую версию протокола, размер и SHA256 архива и хеш внутреннего
манифеста. Разрешение MAIN дополнительно привязано к `agent_id` и сроку
действия. Сам секрет в ZIP не включается и при установке не заменяется.
Ограничение модели: общий симметричный секрет находится и на MAIN, и на
RX-ноутбуке. Компрометация одного из этих узлов позволяет создавать корректные
HMAC-подписи. Аппаратного корня доверия и независимой издательской подписи у
этого контура нет.
Перед переключением агент скачивает архив не более двух раз за одну
сохранённую попытку обновления, проверяет подпись, размер, SHA256, точный список
файлов, хеш каждого файла, допустимость путей, предел распакованного объёма,
совместимость и свободное место. Затем выполняется программная самопроверка без
SDR. Если агент занят локальной задачей, обновление остаётся в состоянии
`WAITING_FOR_IDLE`. Наличие файлов само по себе не считается установкой:
фактическую версию MAIN получает из следующего сообщения работающего агента.
## Поиск эталонной лестницы
`STAIRCASE_SEARCH` принимает только идентификатор существующего сеанса и
ожидаемые SHA256 RX-файла и эталона. Агент сам строит пути внутри
`%USERPROFILE%\Documents\SDR_Rover_Runtime`: запись берётся из каталога
`rx_capture`, а эталон — из отдельного каталога `references`. Сетевой запрос не
может передать путь или команду.
Перед запуском агент проверяет оба SHA256. Отчёт сохраняется рядом с записью
под именем `staircase_search_report.json`. Если подходящий отчёт уже существует,
он проверяется и возвращается без повторного анализа и без перезаписи. Для
задачи всегда заданы `hardware_accessed = false`, `rf_tx = 0` и
`automatic_retry = false`.
Поиск выполняется отдельным фиксированным процессом. Агент продолжает опрос
сервера и сообщает фактическую фазу, число обработанных комплексных отсчётов и
общий объём. Эти значения берутся из потокового анализатора, а не оцениваются
по времени. Активная аренда продлевается даже после истечения обычного короткого
тайм-аута. Указатель задачи записывается до запуска процесса, поэтому перезапуск
агента не создаёт второй анализатор. Готовый результат остаётся на диске до
подтверждения MAIN и после обрыва сети отправляется без повторного вычисления.
## Диагностический однократный запуск агента
Команду выполнять из фактического клона `SDR_Rover_Video` на ноутбуке. Пути к
репозиторию, секрету и `hackrf_info.exe` надо подставить по результатам
инвентаризации ноутбука:
```powershell
python -B tools\rf001_rx_http_agent.py `
--server http://192.168.255.185:8765 `
--token-file <путь-к-rf001_http_token.txt> `
--agent-id user-pc `
--repository <путь-к-SDR_Rover_Video> `
--hackrf-info <путь-к-hackrf_info.exe> `
--hackrf-clock <путь-к-hackrf_clock.exe> `
--hackrf-transfer <путь-к-hackrf_transfer.exe> `
--once
```
Без `--once` агент работает постоянно. Этот ручной режим нужен только для
диагностики; при установленном автозапуске его не используют.
## Просмотр результата на основном ПК
```powershell
python -B tools\rf001_http_control.py status
```
Для завершённой инвентаризации сервер показывает состояние задания
`COMPLETE` или `FAILED`, фактические результаты команд и `rf_tx = 0`.
## Автономная проверка 100/100
После подтверждения одинакового HEAD на обоих компьютерах основной ПК ставит
фиксированное программное задание:
```powershell
python -B tools\rf001_http_control.py enqueue-offline-verify `
--agent-id user-pc `
--expected-head a94f2db4ee8377201a97263166cb09431df5f5d6
```
Агент сверяет ветку и HEAD чтением `.git`, поэтому `git.exe` для этой проверки
не нужен. Затем он запускает только два заранее заданных проектных сценария:
создание детерминированной RF-001A волны и её программное декодирование через
встроенный `legacy`-декодер, не требующий Numba. Рабочие
файлы сохраняются вне Git в `Documents\SDR_Rover_Runtime\offline_verify`.
Успех требует ровно 100 CRC-корректных и 100 точных пакетов. HackRF при этом не
открывается, `hardware_accessed = false`, `rf_tx = 0`.
## Подготовленный RX-захват
Сервер не примет постановку RX-захвата без точной локальной фразы
`AUTHORIZE RF RX`. До отдельного разрешения эту команду не выполнять:
```powershell
python -B tools\rf001_http_control.py enqueue-rx-capture `
--agent-id user-pc `
--expected-head a94f2db4ee8377201a97263166cb09431df5f5d6 `
--authorize-rx "AUTHORIZE RF RX"
```
Агент перед захватом проверяет:
- ветку и HEAD без обязательного наличия `git.exe`;
- отсутствие конфликтующих SDR-процессов;
- серийный номер RX `000000000000000097c063dc2e5327d3`;
- строку `clock signal detected` от `hackrf_clock.exe`;
- не менее `6000000000` свободных байт;
- неизменность частоты `1400000000` Гц, частоты дискретизации `8000000`
отсчётов/с, LNA/VGA `24/24` дБ, выключенных RF amp и antenna bias.
Для ручного барьера TX захват длится 300 секунд: `2400000000` комплексных
отсчётов, ожидаемый файл `4800000000` байт. Значение 20 секунд в исходном
профиле проверяется как часть исходных данных, но управляющий контур расширяет
окно до 300 секунд. Это зафиксировано в журнале сеанса. Автоматического повтора
нет.
Фоновый процесс запускает фиксированный Python-runner. Старый
`capture_process_runner.ps1` и Git watcher-ы не изменены и остаются резервом.
Состояние конкретного сеанса запрашивается отдельной фиксированной задачей:
```powershell
python -B tools\rf001_http_control.py enqueue-rx-status `
--agent-id user-pc `
--session-id <SESSION_ID>
```
TX разрешён только при свежем результате `READY`, открытом окне захвата и после
точной отдельной фразы `AUTHORIZE RF TX`. В сетевом протоколе операции TX нет.
После завершения захвата сервер может поставить потоковое декодирование с тремя
фактическими временами. Отчёт и IQ остаются на RX-ноутбуке вне Git.
## Текущее продолжение
На RX-ноутбуке работает версия агента `2026.09.29.2`; штатное обновление через
подписанный HTTP-канал и полный `STAIRCASE_SEARCH` завершены. Дальнейшая работа
с существующим сеансом является только потоковой офлайн-диагностикой сохранённой
IQ-записи. Новый RX-захват и любой RF TX требуют нового отдельного разрешения;
разрешения, использованные в завершённом сеансе, повторно не применяются.

View File

@@ -0,0 +1,157 @@
# RF001 launcher/health diagnostic
Дата проверки: 29 сентября 2026 года.
## Ограничения проверки
- Производственный RX-агент не останавливался и не изменялся.
- Задание Планировщика Windows не изменялось.
- Диагностический процесс не обращался к HTTP-очереди.
- Доступ к SDR не выполнялся: `hardware_accessed=false`, `RF RX=0`, `RF TX=0`.
- Bootstrap `rf001-bootstrap-2026.09.29.5` не создавался; `.4` повторно не запускался.
- Clean release A `rf001-agent-2026.09.29.1` использовался только для чтения кода health writer и проверки `READY.json`/manifest.
## Безопасное состояние после проверки
- Активный производственный агент: `2026.09.25.1`; heartbeat на MAIN свежий, active task отсутствует.
- Незавершённых задач: 0. Незавершённых аппаратных задач: 0.
- Задание `SDR Rover RX Agent`: `Running`; его действие по-прежнему запускает legacy agent, а не release A.
- Производственная цепочка legacy agent на RX: venv wrapper PID 7980 -> базовый Python PID 17568.
- `hackrf_transfer.exe`: 0 процессов. Процессов RX capture: 0.
- Диагностических процессов: 0.
- Clean release A: 10 файлов из 10 совпадают с `versions/rf001-agent-2026.09.29.1` внутри подписанного bootstrap `.4` по относительному пути, размеру и SHA256.
- Conflict quarantine сохранён. SHA256 его diagnostic manifest: `b92f15dd4b7cffef375fd108f454a41623ed7328220c73c3f2b847dc59e956bc`.
- `rf001-bootstrap-2026.09.29.5.zip` отсутствует.
## Доказанная первопричина
Два последовательных изолированных запуска через настоящий `start_release()` дали одинаковый результат. Поля `release_id`, `agent_version`, `launch_nonce` и `state` совпали. Единственным несовпавшим полем стало `health.pid == Popen.pid`.
На проверенном Windows/venv способе запуска `Popen.pid` принадлежит процессу `venv\\Scripts\\pythonw.exe`, а фактический интерпретатор, который исполняет health writer, является его единственным прямым дочерним процессом с образом базового `Python313\\python.exe`. Связь установлена не по именам: сверены PID, PPID, время создания, полный путь образа, командная строка и одноразовый `launch_nonce`.
### Запуск 1
- Launcher процесса диагностики: PID 7848.
- `Popen.pid`: 10284, образ `C:\\Users\\Professional\\Documents\\Codex\\SDR_Rover_Video\\.venv\\Scripts\\pythonw.exe`, PPID 7848, создан `2026-09-29T13:04:44.696889Z`.
- Health writer: PID 8452, образ `C:\\Users\\Professional\\AppData\\Local\\Programs\\Python\\Python313\\python.exe`, PPID 10284, создан `2026-09-29T13:04:44.702070Z`.
- Health обновлён `2026-09-29T16:04:44.866156+03:00`.
- Wrapper уже наблюдался 0,167845 с и отсутствовал к завершению прогона через 2,589228 с после создания.
- Agent child уже наблюдался 0,164086 с и отсутствовал к завершению прогона через 2,585469 с после создания.
- Точный момент завершения каждого процесса не опрашивался, поэтому приведены проверенные нижняя и верхняя границы, а не предполагаемая длительность.
### Запуск 2
- Launcher процесса диагностики: PID 7848.
- `Popen.pid`: 3504, образ `C:\\Users\\Professional\\Documents\\Codex\\SDR_Rover_Video\\.venv\\Scripts\\pythonw.exe`, PPID 7848, создан `2026-09-29T13:04:47.290751Z`.
- Health writer: PID 14616, образ `C:\\Users\\Professional\\AppData\\Local\\Programs\\Python\\Python313\\python.exe`, PPID 3504, создан `2026-09-29T13:04:47.295668Z`.
- Health обновлён `2026-09-29T16:04:47.461326+03:00`.
- Wrapper уже наблюдался 0,169277 с и отсутствовал к завершению прогона через 2,574777 с после создания.
- Agent child уже наблюдался 0,165658 с и отсутствовал к завершению прогона через 2,571158 с после создания.
- Точный момент завершения каждого процесса не опрашивался.
Оба `launch_nonce` различались:
- `96acc3af17150f42d393238bcbdad3ff`;
- `e4a618547e4bcda4b6914bf81bbd7963`.
В обоих случаях штатный `start_release()` завершился fail-closed:
```text
RuntimeError: health file does not match the launched process
```
После каждого прогона процессов, принадлежащих диагностическому запуску, не осталось. Исходные health-файлы и снимки процессов сохранены на RX в:
```text
C:\Users\Professional\Downloads\SDR_Rover_Diagnostics\launcher_health_20260929T130000Z\evidence
```
## Сравнение полей health
| Поле | Запуск 1 | Запуск 2 |
|---|---:|---:|
| `release_id == rf001-agent-2026.09.29.1` | PASS | PASS |
| `agent_version == 2026.09.29.1` | PASS | PASS |
| `launch_nonce` | PASS | PASS |
| `state == RUNNING` | PASS | PASS |
| `health.pid == Popen.pid` | FAIL: 8452 != 10284 | FAIL: 14616 != 3504 |
Следовательно, `Popen.pid` соответствует варианту C: это родитель фактического процесса агента. Он не является произвольным или посторонним процессом; это venv-обёртка с ожидаемой командной строкой, которая существует одновременно с прямым дочерним health writer.
## Строгая модель идентичности
Основным вариантом по-прежнему должен быть запуск, при котором `Popen.pid` принадлежит фактическому долгоживущему агенту. Для проверенной Windows/venv топологии, где это условие не выполняется, допустима только следующая строгая альтернатива:
1. Сохранить точные проверки `release_id`, `agent_version`, одноразового `launch_nonce` и `state=RUNNING`.
2. Снять идентичность процесса `Popen` сразу после запуска: PID, путь образа, командную строку и время создания.
3. Если health PID отличается, принять только один живой прямой дочерний процесс этого конкретного wrapper.
4. Для дочернего процесса проверить время создания после начала данного запуска, точную команду с тем же agent entrypoint и nonce, ожидаемый путь интерпретатора и наличие процесса в момент принятия health.
5. Не принимать внука, произвольного потомка, самый новый `python.exe`, совпадение только по имени или один числовой PID.
6. При завершении или откате повторно сверить PID и время создания перед остановкой wrapper и строго связанных с ним потомков. Это закрывает повторное использование PID.
Минимальное производственное исправление можно ограничить `rf001_agent_launcher.py`: добавить проверку уникального прямого потомка и такую же строгую очистку потомков при ошибке. Формат health менять не требуется, поскольку в нём уже есть nonce и собственный PID. Если получение надёжного времени создания и пути образа без дополнительного компонента окажется невозможно, health writer следует расширить полями process identity; отключать проверку PID нельзя.
## Конечный автомат launcher
Названия ниже описывают фактические фазы кода. Не все они сейчас записываются как отдельное значение в `launcher_status.json`.
| Фаза | Допустимый вход | Процессы | Canonical release / active pointer | Scheduled Task | Health | Действие при ошибке |
|---|---|---|---|---|---|---|
| `LEGACY_ACTIVE` | Действующий агент здоров и idle | Legacy agent запущен | Указатель на legacy | Launcher не меняет определение задания | Legacy `RUNNING` | Запрос обновления принимается только при `safe_to_switch=true`, совпадающем `from_release_id` и idle health |
| `TARGET_VERIFIED` | Подписанный target прошёл `validate_ready_release()` | Новый процесс ещё не создан | Target уже существует в canonical releases; указатель ещё legacy | Без изменений | Legacy health | Отказ до остановки legacy |
| `STARTING_TARGET` | Target проверен | Legacy останавливается; создаётся wrapper target | Указатель атомарно меняется на target; status `STARTING_UPDATE` | Без изменений | Старый health больше не считается подтверждением | Переход в `TARGET_FAILED` |
| `WAITING_FOR_HEALTH` | `Popen` создан, nonce уникален | Wrapper и возможный прямой child | Указатель target | Без изменений | Ожидаются свежие точные поля и строгая process identity | Timeout, exit или mismatch завершают target fail-closed |
| `TARGET_HEALTHY` | Все health-поля и identity прошли | Target продолжает работу | Указатель target; status `RUNNING` | Без изменений | Target `RUNNING` | Дальнейший выход ведёт в `RESTARTING` |
| `TARGET_FAILED` | Startup exit, timeout или mismatch | Target должен быть остановлен без orphan | Target ещё был активным указателем | Без изменений | Ошибочный health не принимается | Начать rollback |
| `ROLLING_BACK` | Target не прошёл health gate | Target очищается, запускается legacy | Указатель возвращается на legacy | Без изменений | Для legacy требуется новый nonce и новый health | Ошибка rollback должна оставаться явной, а не считаться успехом |
| `LEGACY_RESTORED` | Legacy снова прошёл health gate | Legacy работает | Указатель legacy; status `ROLLED_BACK` | Без изменений | Свежий legacy `RUNNING` | Архивировать запрос и продолжить legacy |
Существенный пробел текущего production-кода: `TARGET_FAILED` останавливает только объект `Popen`; общая строгая очистка потомка в launcher пока не реализована. В двух диагностических запусках orphan не осталось, но это наблюдение не заменяет будущую явную очистку по identity.
## Проверенные гонки и регрессии
Локальный набор `pytest` проверяет:
- точное совпадение процесса: PASS;
- неверные nonce, release, version и state: FAIL;
- stale health: FAIL;
- посторонний PID: FAIL;
- единственный ожидаемый прямой child: PASS только со строгой lineage, nonce, командой и временем создания;
- неожиданный grandchild: FAIL;
- повторное использование PID с другим creation time: FAIL;
- target умер до health: FAIL;
- target записал health, но умер: FAIL;
- health в состоянии `STARTING`, до полной готовности: FAIL;
- rollback не принимает health с nonce прошлого запуска: FAIL;
- выбор процессов для cleanup не затрагивает посторонний процесс;
- cleanup останавливает только строго принадлежащие wrapper и child;
- два успешных логических запуска детерминированы и имеют разные nonce;
- health JSON заменяется атомарно.
Команда проверки:
```text
python -m pytest tests/test_rf001_launcher_health_diagnostic.py tests/test_rf001_agent_launcher.py tests/test_rf001_rx_http_agent.py tests/test_rf001_agent_update.py -q
```
Результат: `48 passed`, предупреждение только о недоступности `.pytest_cache` в текущей песочнице.
Windows integration test использовал реальный RX venv, установленный launcher source, canonical release A и настоящий agent health writer. Выполнено два последовательных изолированных запуска. HTTP polling, SDR и аппаратные команды в этом режиме отсутствовали.
## Файлы и следующий этап
В этой диагностической фазе добавлены только:
- `tools/rf001_launcher_health_diagnostic.py`;
- `tests/test_rf001_launcher_health_diagnostic.py`;
- этот отчёт.
Для будущего производственного исправления потребуются как минимум:
- `tools/rf001_agent_launcher.py`;
- `tests/test_rf001_agent_launcher.py`;
- тесты упаковки, подтверждающие неизменность подписанного состава и fail-closed rollback.
`tools/rf001_rx_http_agent.py` потребуется менять только при выборе расширенного health process identity; доказанная минимальная схема может обойтись без изменения агента.
После отдельного согласования исправления понадобится новый уникальный bootstrap, поскольку launcher на RX должен быть заменён проверенным пакетом. Bootstrap `.5` в рамках этой фазы не создавался.