Harden RF node control and agent updates
This commit is contained in:
223
docs/handoff/RF001_BOOTSTRAP_20260929_5_ATTEMPT.md
Normal file
223
docs/handoff/RF001_BOOTSTRAP_20260929_5_ATTEMPT.md
Normal 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` для этого исправления не требуется.
|
||||
310
docs/handoff/RF001_HTTP_CONTROL.md
Normal file
310
docs/handoff/RF001_HTTP_CONTROL.md
Normal 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 требуют нового отдельного разрешения;
|
||||
разрешения, использованные в завершённом сеансе, повторно не применяются.
|
||||
157
docs/handoff/RF001_LAUNCHER_HEALTH_DIAGNOSTIC.md
Normal file
157
docs/handoff/RF001_LAUNCHER_HEALTH_DIAGNOSTIC.md
Normal 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` в рамках этой фазы не создавался.
|
||||
Reference in New Issue
Block a user