diff --git a/openspec/changes/nats-env-config/.openspec.yaml b/openspec/changes/nats-env-config/.openspec.yaml new file mode 100644 index 0000000..b9ebfc7 --- /dev/null +++ b/openspec/changes/nats-env-config/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-12 diff --git a/openspec/changes/nats-env-config/design.md b/openspec/changes/nats-env-config/design.md new file mode 100644 index 0000000..fadc2ae --- /dev/null +++ b/openspec/changes/nats-env-config/design.md @@ -0,0 +1,79 @@ +## Context + +Решение владельца 12.10.2026: «топики можно выставлять через секреты и jwt токен тоже, т.к. у нас будут различные наборы +топиков и разные jwt для прод и тест окружения». Выбран подход 1 из трёх обсуждённых (12.10.2026): + +1. **Выбран:** три ключа в группе `OPENBAO__NATS__`, одинаковые в трёх сервисах. +2. `OPENBAO__APP__*` (`settings_sources.py`) — отвергнут: разносит JWT и subject'ы, на которые он даёт права, по разным + группам. +3. Один префикс `NATS_SUBJECT_PREFIX` — отвергнут: работает, только если тестовые subject'ы платформы отличаются от + боевых префиксом. + +Образец — `RSHB/armka_file_worker` (проверен в контуре): `NATSSettings` с `NATS_URL`, `NATS_JWT: SecretStr`, +`NATS_SEED: SecretStr`, `NATS_COMMAND_SUBJECT`, `NATS_EVENT_SUBJECT`; `Settings(hide_input_in_errors=True)`. + +| Ключ | Умолчание | arm-core | tabular-parser | scan-gateway | +|---|---|---|---|---| +| `OPENBAO__NATS__NATS_TABULAR_REQUEST_SUBJECT` | `files.tabular_uploaded` | публикует | подписан (queue `tabular-parser`) | — | +| `OPENBAO__NATS__NATS_IMAGE_REQUEST_SUBJECT` | `files.img_uploaded` | публикует | — | подписан (queue `scan-gateway`) | +| `OPENBAO__NATS__NATS_RESULT_SUBJECT` | `files.tabular_parsed` | подписан (queue `arm-core`) | публикует | публикует | + +## Goals / Non-Goals + +Goals: subject'ы и учётные данные NATS задаются секретами окружения; разъехавшиеся настройки видны в `/healthcheck`; +тесты не делят subject'ы со стендом. Non-goals: см. proposal. + +## Decisions + +1. **Ключи одинаковые во всех трёх сервисах, проверяются все три.** На окружение — один набор секретов, который + платформа кладёт всем сервисам; сервис использует только свои subject'ы, но валидирует все три, чтобы ошибка набора + проявилась в любом сервисе одинаково. +2. **Валидация при старте.** Имя subject'а — непустое, без пробельных символов, без шаблонов `*` и `>`, без пустых + токенов (`a..b`, ведущая или завершающая точка). Три имени попарно различны: совпадение запроса и ответа — сервис + получает собственные ответы; совпадение двух запросов — каждый запрос получают оба сервиса. Ошибка — сервис не + стартует (ошибка конфигурации, как у `DB_SCHEMA`). +3. **Константы `contract/subjects.py` — только умолчания.** Код публикации и подписок получает имена из настроек; + функция выбора subject'а по `pipeline` принимает настройки (форма — на усмотрение плана сервиса). +4. **Секреты.** `NATS_JWT` и `NATS_SEED` — `SecretStr`; в колбэки nkeys значение передаётся через + `get_secret_value()`; `Settings` с `hide_input_in_errors=True`, чтобы ошибка валидации конфигурации не печатала + входные значения. +5. **`/healthcheck`.** Новый объект `subjects` с subject'ами, которые использует сервис: arm-core — + `tabular_request`, `image_request`, `result`; tabular-parser — `tabular_request`, `result`; scan-gateway — + `image_request`, `result`. Отдаётся всегда, в том числе при `nats = off | down` (это конфигурация, не состояние). + Решение владельца 12.10.2026: «пусть будет в хэлзчеке». +6. **Лог при старте.** Сервис пишет свои subject'ы одной строкой при старте (существующая строка «Подписка NATS + активна» сохраняется). +7. **Тесты (OQ-83).** Интеграционные тесты `tabular-parser` и `scan-gateway`, где обе стороны обмена — в самом тесте, + задают уникальные subject'ы через настройки и не пересекаются с контейнерами стенда. e2e ядра (`make e2e`) остаётся + на subject'ах по умолчанию: он гоняет настоящий контейнер `tabular-parser` стенда, который слушает умолчания, а + конфликт там — в общей queue group `arm-core` на subject'е ответа; требование останавливать ядро стенда сохраняется + (R-7). Интеграционные тесты NATS ядра уже работают на уникальных subject'ах. + +## Risks / Trade-offs + +- Разъехавшиеся настройки ядра и сервиса — тишина на шине: ядро переведёт задания в `dlq` по `NO_ACK`. Смягчение: + `subjects` в `/healthcheck` и лог при старте; в `03-architecture.md` — требование задавать набор всем трём сервисам. +- JWT окружения без прав на заданные subject'ы — ошибка прав NATS при подписке/публикации (в логе сервиса); смягчение + — перечень секретов окружения в `03-architecture.md` и `contour-transfer-guide.md`. +- `/healthcheck` публичен и теперь раскрывает имена subject'ов. Имена не секретны (без JWT ими не воспользоваться), + JWT и seed в ответ не попадают. + +## Decisions made without discussion + +1. 12.10.2026 — **R-1:** имена ключей `NATS_TABULAR_REQUEST_SUBJECT`, `NATS_IMAGE_REQUEST_SUBJECT`, + `NATS_RESULT_SUBJECT` (по образцу FileWorker — префикс `NATS_`, суффикс `_SUBJECT`), а не `NATS_COMMAND_SUBJECT` / + `NATS_EVENT_SUBJECT` FileWorker: у нас два subject'а запроса. Имена показаны владельцу в дизайне и приняты вместе + с подходом. Цена ошибки: низкая — переименование ключа до выката в контур. +2. 12.10.2026 — **R-2:** `schema_version` не меняется: тела сообщений те же, subject'ы — окружение (как R-2 в + `nats-files-subjects`). Цена ошибки: низкая. +3. 12.10.2026 — **R-3:** проверяются все три subject'а в каждом сервисе, включая неиспользуемые им (решение 1). + Цена ошибки: низкая — лишний отказ старта при кривом наборе, который всё равно сломал бы обмен. +4. 12.10.2026 — **R-4:** `subjects` в `/healthcheck` отдаётся и при `nats = off`. Цена ошибки: низкая. +5. 12.10.2026 — **R-5:** пароли MinIO/SMB в `SecretStr` не переводятся — вне запроса владельца; `hide_input_in_errors` + всё равно скрывает их в ошибках валидации. Цена ошибки: пароли видны в `repr(settings)` до отдельной задачи. +6. 12.10.2026 — **R-6:** маршрут — OpenSpec change `nats-env-config` в каждом из четырёх репозиториев, один план SDD на + все четыре (больше пяти задач — SDD-контроллер). Цена ошибки: низкая, процессная. +7. 12.10.2026 — **R-7:** e2e ядра не переводится на свои subject'ы (решение 7): ему нужен контейнер `tabular-parser` + стенда на умолчаниях, а развести ядро теста и ядро стенда может только своя queue group, которая в этом change + не настраивается. OQ-83 для `make e2e` остаётся открытым. Цена ошибки: перед `make e2e` по-прежнему останавливать + ядро стенда. diff --git a/openspec/changes/nats-env-config/proposal.md b/openspec/changes/nats-env-config/proposal.md new file mode 100644 index 0000000..15042bd --- /dev/null +++ b/openspec/changes/nats-env-config/proposal.md @@ -0,0 +1,60 @@ +## Why + +Прод и тест будут работать с разными наборами subject'ов NATS и разными JWT. Платформа РАИСА передаёт в под только +переменные `OPENBAO__*`, поэтому окружение должно настраиваться только секретами OpenBao, без пересборки и релиза +сервисов. Сейчас JWT и seed уже приходят как `OPENBAO__NATS__NATS_JWT` / `NATS_SEED`, а subject'ы зашиты константами +`api/src/contract/subjects.py` трёх сервисов (решение R-4 в #74) и уже раз менялись (`recognition.*` → `files.*`, +08.10.2026). Решение владельца 12.10.2026: subject'ы и JWT задаются по окружению секретами группы `OPENBAO__NATS__` — +так устроен `RSHB/armka_file_worker`, проверенный в контуре (`NATS_COMMAND_SUBJECT`, `NATS_EVENT_SUBJECT` рядом с +`NATS_JWT`). + +Срез: вне плана срезов `09-delivery-plan.md` — подготовка к контуру (инфраструктура NATS платформы). Issue +RSHB/arm-ka-docs#80. + +## What Changes + +- Три настройки окружения, одинаковые ключи во всех трёх сервисах, умолчания — текущие имена: + `OPENBAO__NATS__NATS_TABULAR_REQUEST_SUBJECT` (`files.tabular_uploaded`), + `OPENBAO__NATS__NATS_IMAGE_REQUEST_SUBJECT` (`files.img_uploaded`), + `OPENBAO__NATS__NATS_RESULT_SUBJECT` (`files.tabular_parsed`). +- Контракт: subject'ы — конфигурация окружения; у ядра и сервисов одного окружения значения MUST совпадать; имена + проверяются при старте (непустые, без пробелов и шаблонов `*`/`>`, три разных). +- `NATS_JWT` / `NATS_SEED` — секреты: значения не попадают в `repr`, логи и тексты ошибок валидации конфигурации. +- Каждый сервис пишет свои subject'ы в лог при старте и отдаёт их в `/healthcheck` (объект `subjects`). +- Интеграционные тесты `tabular-parser` и `scan-gateway` берут собственные subject'ы через настройки и не делят их с + контейнерами стенда (OQ-83 в части этих тестов; e2e ядра — без изменений, design R-7). +- Документы: `context/03-architecture.md` (NATS платформы, перечень секретов), `context/07-open-questions.md` (OQ-83), + `dev/README.md`, `dev/contour-transfer-guide.md`, `dev/qa-guide.md`, `dev/frontend-api-guide.md` (ответ + `/healthcheck`, если описан), `dev/.env.example`. + +Читают и пишут контракт: `arm-core` (публикует запросы в оба subject'а запроса, подписан на subject ответа), +`tabular-parser` (подписан на subject запроса `tabular`, публикует в subject ответа), `scan-gateway` (подписан на +subject запроса `image`, публикует в subject ответа). Дочерние change'и `nats-env-config`: RSHB/arm-core#106, +RSHB/arm-tabular-parser#31, RSHB/arm-scan-gateway#32. + +## Non-goals + +- `schema_version` контракта не меняется: тела сообщений, `Nats-Msg-Id`, файл результата — те же. +- Queue group'ы (`arm-core`, `tabular-parser`, `scan-gateway`) остаются константами. +- Механизм аутентификации NATS не меняется (nkeys seed + JWT, оба пусты — без учётных данных). +- Пароли MinIO и SMB в `SecretStr` не переводятся. +- Subject'ы не настраиваются через `OPENBAO__APP__*`: это группа `NATS`, а не настройки верхнего уровня. +- DBML не поднимается; заметки v5.17 про subject'ы — follow-up RSHB/arm-ka-docs#79 (R-3). +- Отдельный NATS для тестов не поднимается. + +## Capabilities + +### New Capabilities + +### Modified Capabilities +- `recognition-contract`: subject'ы запроса и ответа — настройки окружения с умолчаниями `files.*`. + +## Impact + +- Сервисы: `arm-core`, `tabular-parser`, `scan-gateway` — `core/config.py` (`NATSSettings`), `contract/subjects.py`, + `core/nats.py`, публикация и подписки, `/healthcheck`, тесты, `.env.example`, спеки, README. +- Выкат обратно совместим: без новых ключей всё работает на `files.*`. Для теста платформа кладёт свои `NATS_JWT`, + `NATS_SEED` и три subject'а одинаково всем трём сервисам; JWT окружения должен давать права publish/subscribe на эти + subject'ы. +- Ядро вычисляет subject при каждой публикации по `pipeline`, поэтому после смены настроек повторная публикация + заданий в работе уходит в новый subject; `recognition_job.request_subject` записывает фактический subject. diff --git a/openspec/changes/nats-env-config/specs/recognition-contract/spec.md b/openspec/changes/nats-env-config/specs/recognition-contract/spec.md new file mode 100644 index 0000000..3205397 --- /dev/null +++ b/openspec/changes/nats-env-config/specs/recognition-contract/spec.md @@ -0,0 +1,96 @@ +## ADDED Requirements + +### Requirement: Subject'ы NATS — настройки окружения +Subject'ы обмена SHALL задаваться секретами группы `OPENBAO__NATS__` с одинаковыми ключами во всех трёх сервисах: +`NATS_TABULAR_REQUEST_SUBJECT` (по умолчанию `files.tabular_uploaded`), `NATS_IMAGE_REQUEST_SUBJECT` (по умолчанию +`files.img_uploaded`), `NATS_RESULT_SUBJECT` (по умолчанию `files.tabular_parsed`). У ядра и сервисов одного окружения +значения MUST совпадать. Каждый сервис SHALL при старте проверять все три имени: непустое, без пробельных символов, без +шаблонов `*` и `>`, без пустых токенов, три имени попарно различны; при нарушении сервис MUST NOT стартовать. +Учётные данные NATS (`OPENBAO__NATS__NATS_JWT`, `OPENBAO__NATS__NATS_SEED`) SHALL задаваться по окружению теми же +секретами, и их значения MUST NOT попадать в логи, `repr` настроек и тексты ошибок валидации конфигурации. Каждый +сервис SHALL писать используемые subject'ы в лог при старте и отдавать их в `/healthcheck` объектом `subjects` +(ключи `tabular_request`, `image_request`, `result` — только те, что использует сервис) независимо от состояния NATS. +Queue group'ы и `schema_version` от настроек subject'ов не зависят. + +#### Scenario: Умолчания +- **WHEN** сервис стартует без ключей `OPENBAO__NATS__NATS_*_SUBJECT` +- **THEN** обмен идёт по `files.tabular_uploaded`, `files.img_uploaded`, `files.tabular_parsed` + +#### Scenario: Набор тестового окружения +- **WHEN** всем трём сервисам заданы одинаковые `OPENBAO__NATS__NATS_TABULAR_REQUEST_SUBJECT`, + `NATS_IMAGE_REQUEST_SUBJECT`, `NATS_RESULT_SUBJECT`, `NATS_JWT`, `NATS_SEED` тестового окружения +- **THEN** ядро публикует запросы и получает ack и результаты только по этим subject'ам; subject'ы по умолчанию не + используются, `recognition_job.request_subject` содержит subject тестового окружения + +#### Scenario: Недопустимый набор +- **WHEN** `NATS_RESULT_SUBJECT` совпадает с одним из subject'ов запроса, или имя содержит `*`, `>` или пробел +- **THEN** сервис не стартует с ошибкой конфигурации, называющей ключ + +#### Scenario: Разъехавшиеся настройки видны +- **WHEN** у ядра и сервиса заданы разные subject'ы ответа +- **THEN** `/healthcheck.subjects.result` ядра и сервиса различаются; JWT и seed в ответе `/healthcheck` отсутствуют + +#### Scenario: Ошибка конфигурации не раскрывает JWT +- **WHEN** сервис не стартует из-за ошибки валидации настроек при заданных `NATS_JWT` и `NATS_SEED` +- **THEN** текст ошибки и лог не содержат значений JWT и seed + +## MODIFIED Requirements + +### Requirement: Сообщение запроса распознавания +Тело сообщения на subject запроса `tabular` или `image` (настройки окружения `NATS_TABULAR_REQUEST_SUBJECT` / +`NATS_IMAGE_REQUEST_SUBJECT`, по умолчанию `files.tabular_uploaded` / `files.img_uploaded`) SHALL соответствовать +`docs/contracts/recognition-request.schema.json` v1.0: обязательные поля `schema_version` (`"1.0"`), `job_id` (uuid = +`recognition_job.id`), `file_id` (uuid = `stored_file.id`), `pipeline` (`tabular` | `image`), `file_type` +(`XLS` | `XLSX` | `XLSB` | `PDF` | `PNG` | `JPG`), `original_name`, `s3 {bucket, key}` исходника в роли `uploads`, +`reply_deadline` (date-time, UTC), `attempt` (≥ 1). Subject SHALL соответствовать `pipeline`. + +#### Scenario: Публикация запроса ядром +- **WHEN** ядро переводит `recognition_job` из `queued` в `published` +- **THEN** в subject, соответствующий `recognition_job.pipeline`, публикуется сообщение, проходящее схему запроса, с + заголовком `Nats-Msg-Id` = `:` + +#### Scenario: Сообщение не по схеме +- **WHEN** сервис получает запрос, не проходящий схему (нет `s3.key`, неизвестный `file_type`), но из тела извлекаются + корректные `job_id` и `file_id` +- **THEN** сервис публикует результат со `status = error`, `error.code = SCHEMA_MISMATCH` **без предшествующего ack + `processing`** и не падает; `attempt` для `Nats-Msg-Id` результата берётся из тела запроса, при его отсутствии + реализации расходятся (`tabular-parser` берёт `attempt` из заголовка `Nats-Msg-Id` запроса или не отвечает, + `scan-gateway` подставляет 1) — см. OQ-71 + +#### Scenario: Нечитаемый запрос +- **WHEN** сервис получает сообщение, из которого нельзя извлечь `job_id` (не JSON, не объект, `job_id` отсутствует или + не uuid), а значит нельзя собрать валидный конверт +- **THEN** сервис ответа не публикует: запрос записывается в лог и отбрасывается; ядро не получит ack, повторит + публикацию `OUTBOX_MAX_REPUBLISH` раз и переведёт задание в `dlq` (`error_code = NO_ACK`) + +#### Scenario: Невалидный повтор запроса, уже находящегося в работе +- **WHEN** сервис получает запрос, не проходящий схему, но с `job_id`, который уже в работе или обработан этим процессом +- **THEN** конверт `SCHEMA_MISMATCH` не публикуется (его `Nats-Msg-Id` `result::` совпал бы с настоящим + результатом, и ядро отбросило бы настоящий как дубликат); запрос записывается в лог и отбрасывается + +#### Scenario: Примеры контракта — иллюстрации +- **WHEN** запрос берётся из `docs/contracts/examples/request-*.json` (в `request-image.json` `reply_deadline` лежит в + прошлом) +- **THEN** он проходит схему: схема проверяет для `reply_deadline` лишь формат `date-time`, но не сравнивает срок с + текущим временем; пример — иллюстрация формы сообщения, а не готовое к исполнению задание; проверка срока — дело + сервиса (`scan-gateway` отвечает `error TIMEOUT`, если `reply_deadline` истёк до отправки файла в ИС Сканирование) + +### Requirement: Сообщение результата распознавания +Тело сообщения на subject ответа (настройка окружения `NATS_RESULT_SUBJECT`, по умолчанию `files.tabular_parsed`) +SHALL соответствовать `docs/contracts/recognition-result-message.schema.json` +v1.0: обязательные поля `schema_version` (`"1.0"`), `job_id`, `file_id`, `status` (`processing` | `done` | `needs_check` | +`error`), `service {name, version}`; при `status ∈ {done, needs_check}` обязательны `result_s3 {bucket, key}` и +`result_schema_version`; при `status = error` обязателен `error {code, message}`; опциональны `raw_s3_key` +(только `scan-gateway`) и `metrics`. Поле `group_schema` в конверт NOT включается — источник `recognition_job.group_schema` +— файл результата. + +#### Scenario: Успешный результат +- **WHEN** сервис завершил обработку файла +- **THEN** файл результата записан в S3 по ключу `results/{file_id}/{job_id}/result.json`, а затем опубликован конверт + со `status = done | needs_check`, `result_s3` указывает на этот файл, заголовок `Nats-Msg-Id` = `result::` + +#### Scenario: Ошибка обработки +- **WHEN** сервис не смог обработать файл (нечитаем, таймаут внешней системы, тип не поддерживается) +- **THEN** публикуется конверт со `status = error` и `error.code` из перечня схемы (`UNSUPPORTED_FORMAT`, `UNREADABLE`, + `TIMEOUT`, `SCHEMA_MISMATCH`, `NOT_IMPLEMENTED`, `FILE_READ_ERROR`, `INVALID_FORMAT`, `INVALID_DOCUMENT`, + `RECOGNITION_FAILED`, `UNKNOWN_ERROR`); `result_s3` может отсутствовать diff --git a/openspec/changes/nats-env-config/tasks.md b/openspec/changes/nats-env-config/tasks.md new file mode 100644 index 0000000..4f8d767 --- /dev/null +++ b/openspec/changes/nats-env-config/tasks.md @@ -0,0 +1,26 @@ +## 1. Контракт (docs) + +- [ ] 1.1 Дельта спеки `recognition-contract` (новое требование «Subject'ы NATS — настройки окружения», два изменённых); + Purpose спеки при архивации: subject'ы — настройки с умолчаниями `files.*` +- [ ] 1.2 `contracts/recognition-request.schema.json`, `contracts/recognition-result-message.schema.json`: в описаниях + subject'ов — «настройка окружения, по умолчанию …» (только `description`, `schema_version` не меняется; копии в + сервисах обновляются побайтно) + +## 2. Сервисы (дочерние change'и `nats-env-config`) + +- [ ] 2.1 RSHB/arm-core#106: `NATSSettings` (3 subject'а, `SecretStr`, валидация), `hide_input_in_errors`, публикация и + подписка по настройкам, `/healthcheck.subjects`, лог при старте, unit-тесты (e2e — на умолчаниях, design R-7), `.env.example`, спека `recognition-job-exchange`, README +- [ ] 2.2 RSHB/arm-tabular-parser#31: то же для подписки `tabular_request` и публикации `result`, спека + `tabular-recognition-service` +- [ ] 2.3 RSHB/arm-scan-gateway#32: то же для подписки `image_request` и публикации `result`, спека + `image-recognition-gateway` + +## 3. Документы контекста + +- [ ] 3.1 `context/03-architecture.md`: subject'ы — настройки окружения (таблица потоков, NATS платформы), перечень + секретов `OPENBAO__NATS__*` по сервисам, требование одинакового набора для всех трёх сервисов +- [ ] 3.2 `dev/README.md` (таблица переменных, `/healthcheck`, интеграционные тесты), `dev/.env.example`, + `dev/contour-transfer-guide.md` (секреты окружения), `dev/qa-guide.md` и `dev/frontend-api-guide.md` (ответ + `/healthcheck`, если описан), `repo-templates/*.CLAUDE.md` при упоминании констант subject'ов +- [ ] 3.3 `context/07-open-questions.md`: OQ-83 — интеграционные тесты `tabular-parser`/`scan-gateway` на своих + subject'ах; для e2e ядра вопрос остаётся (общая queue group `arm-core`, design R-7)