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,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` в рамках этой фазы не создавался.