22. Ограничить кол-во символов в сообщении на фронте. Показывать в моменте счетчик: n/max, где n сколько символов уже напечатано, max сколько может быть отправлено. Максимальное кол-во символов - положить в app_settings.
7. Убрать с экрана ввода номера телефона тексты согласий внизу экрана: Нажимая «Получить код», вы соглашаетесь с условиями использования и политикой конфиденциальности. Согласия пользователь дает ранее на отдельном экране.
# В разработку:
3. Унифицировать сообщения гостевого режима о необходмости
На экране профиля в гостевом режиме добавить кнопку "Авторизоваться"
2. После интеграции с смс провайдером, реализовать debounce механизм при авторизации - каждая след. смс можно отправить через все большее окно.
3. аудит безопасности вм
3. На экране профиля в гостевом режиме добавить кнопку "Авторизоваться"
5. Store-review вход: точечный bypass в Keycloak OTP SPI по номеру из `.env` (`STORE_REVIEW_ENABLED` / `STORE_REVIEW_PHONE` / `STORE_REVIEW_OTP`) — для этого телефона SMS не шлётся, verify принимает фиксированный OTP; остальные номера идут обычным OTP/SMS. Не путать с глобальным `KEYCLOAK_OTP_MOCK_*`. Учётные данные только в Review Notes стора (не в бинарнике/UI); пользователь с демо-контентом; в production включать только на время ревью.
6. Хранить историю устройств, с которых пользователь входил в ЛК (Ид юзера, идентификатор устройства, дата последнего входа, способ входа - веб\приложение)
9. Веб-пуши для PWA
@@ -40,6 +47,8 @@
18. Разработка notification-service
20. Поднять второй контур для продакшн
21. Спрятать сеть за балансировщиком нагрузки
22. Автопродление TLS падает при перезагрузке nginx; сертификат действует до 14.10.2026. (Исправить reload внутри контейнера и проверить systemctl start an-chat-ssl-renew.service до успешного завершения.)
23. WireGuard-only SSH.
24. Добавить логи (Для Python-сервисов добавить OTLP Log Exporter: api-backend; sms-service; sms-worker. Подключить LoggerProvider, BatchLogRecordProcessor и bounded queue. Передавать resource attributes: service.name; service.version; deployment.environment; service.namespace=han-chat.) Экспортировать структурированные поля request_id, trace_id, span_id, severity и event name. Оставить stdout как аварийный локальный журнал. Добавить canary-тесты, запрещающие экспорт токенов, cookie, телефонов, email, текстов сообщений, SQL и object keys.).
25. Nginx metrics/tracing в signoz
@@ -62,3 +71,41 @@ debounce на отправку СМС (сейчас есть Фиксирова
5. Подключить OTLP-провайдер
6. Починить баги
7. Второй контур для продакшн
# Переезд на тестовый домен
**Нет — одного `.env` и новых сертификатов недостаточно.**
Нужно пройти цепочку:
### 1. DNS
`A`-запись нового домена → IP ВМ (до выпуска сертификата).
- **Keycloak client** — redirect URIs / web origins (в realm сейчас зашиты конкретные домены вроде `chat.han0107.ru`)
- **i-Digital** — callback URL, если провайдер его фиксирует
Итого: `.env` + сертификат — ядро, но без DNS, CORS (API + S3), rebuild frontend, Keycloak hostname/redirects, Bitrix URL и seed CORS логин/загрузки/интеграции сломаются.
Сервис — internal stub для проверки orchestration `api-backend`, а не реальный moderation/antivirus engine. Он доступен только в Docker network и реализует канонические пути arch-02:
`message-safety` — внутренний сервис, который до отправки сообщения в Bitrix24 проверяет пользовательский текст, содержащиеся в нём ссылки и файлы из S3-quarantine.
Спецификация детализирует архитектуру, но не меняет её. При конфликте приоритет имеют `arch-00`…`arch-05`. Канонические domain outcomes:
По базовой архитектуре final deny у Message Safety обычно `403`. Для этой заглушки пользователь явно задал особый task-контракт: `GET task` независимо возвращает примерно с равной вероятностью `203`, `200` или **`400`**.
- `200 allow`;
- `403 deny`;
- `203 pending` с последующим sticky `200` или `403`.
Здесь `400` на валидном `GET task` — **финальный отрицательный verdict/error заглушки**, а не malformed HTTP request. `api-backend` обязан трактовать его как terminal safety rejection и отображать публично как `422 message_blocked`, выставляя `safety_status=blocked`, `delivery_status=rejected`, без вызова Bitrix. Клиенту raw internal `400` не проксируется.
Test-only правила по первому символу, случайные verdict и terminal `400 stub_final_error` в production-контракт не входят.
Это намеренное test-only расширение текущей таблицы arch-02 (`200/203/403`). Перед использованием не как заглушки arch-02 и contract tests должны быть обновлены либо `400` должен быть заменён на канонический `403`. Существующие arch-файлы в рамках этой задачи не изменяются.
## 2. Границы ответственности
## 3. Технологический профиль
### 2.1. Сервис отвечает за
- Python 3.12+, FastAPI, Pydantic v2, Uvicorn.
- Redis asyncio client, DB2.
- OpenTelemetry, JSON logging.
- pytest/anyio, HTTPX ASGI client, real Redis integration tests.
- Без PostgreSQL и S3 для этой stub-реализации; их будущая интеграция находится вне scope.
- строгую валидацию internal DTO;
- нормализацию и rule-based проверку текста;
- извлечение и проверку ссылок;
- валидацию file metadata и фактического формата;
- чтение файла из S3-quarantine по read-only credentials;
- вычисление authoritative SHA-256;
- антивирусную проверку файла через ClamAV;
- выбор sync/async режима;
- создание и исполнение async safety tasks;
- sticky final verdict, verdict cache и audit в схеме `message_safety`;
- task coordination/cache/rate limits в Redis DB2;
- internal API, health, метрики, трассировку и безопасные JSON-логи.
## 4. Приоритет правил
### 2.2. Сервис не отвечает за
Перед классификацией текст нормализуется. Правила применяются строго в порядке:
- JWT пользователя, согласия и авторизацию доступа пользователя к диалогу;
- edge/API rate limits;
- загрузку файла и выдачу presigned URL;
- запись пользовательского сообщения и статусов в `han_app`;
- copy/promote файла из quarantine в S3-data и удаление объекта;
- доставку в Bitrix24, realtime и пользовательский текст ошибки;
- анализ входящих сообщений оператора;
- ML-модерацию смысла, токсичности или правдивости текста.
1. validation/auth: invalid DTO или service token обрабатываются до бизнес-правил;
2. нормализация;
3. если первый Unicode code point нормализованного текста — кириллическая `ф` или `Ф`, вернуть `403 deny`;
4. иначе если первый code point — десятичная цифра, создать task и вернуть `203 pending`;
5. любой иной текст, включая пустой после допустимой нормализации, вернуть `200 allow`.
Этими операциями владеет `api-backend` или соответствующий архитектурный модуль.
Таким образом, после нормализации строка не может одновременно начинаться и с `ф/Ф`, и с цифры. Rule `ф/Ф` записан раньше для явности. Для file-only request без текста default — `200 allow`; заглушка не сканирует файл.
## 3. Threat model MVP
## 5. Нормализация
### 3.1. Текст и ссылки
Детерминированный pipeline:
1. требовать JSON UTF-8;
2. заменить `CRLF/CR` на `LF`;
3. Unicode normalization `NFKC`;
4. удалить leading Unicode whitespace (`lstrip`);
5. не менять регистр всей строки и не удалять punctuation;
6. ограничить текст max length до значения internal DTO (ориентир 10 000 code points).
| ReDoS/DoS правилами | Линейные/ограниченные regex, лимиты текста, URL и времени | `400` или dependency error |
«Цифра» означает Unicode category `Nd` после NFKC, не только ASCII `[0-9]`.
Rules не заменяют безопасный rendering. Frontend и Bitrix integration обязаны экранировать текст; safety является дополнительным барьером, а не HTML sanitizer.
## 6. Authentication и common headers
### 3.2. Файлы
Каждый `/internal/safety/v1/*` требует:
| Угроза | Контроль | Результат |
|---|---|---|
| Недопустимый размер/MIME | Сверка DTO с allow-list и лимитами | `403 deny` |
| Архивная бомба/ресурсное истощение | Лимиты размера, stream scan, ClamAV limits/timeouts | deny при policy hit; error при сбое |
| Polyglot/неоднозначный формат | Строгий формат detector и deny при mismatch/ambiguity | `403 deny` |
| Повтор известного файла | Cache по SHA-256 + versions | Sticky cached verdict |
MVP принимает только типы из `chat.attachments.allowed_extensions` и `chat.attachments.allowed_mime_types`, при `chat.attachments.max_size_mb`. Расширение проверяет `api-backend` до вызова safety; `message-safety` независимо проверяет MIME и фактический формат байтов. Internal DTO не содержит имени файла, поэтому сервис не выводит расширение из object key.
### 3.3. Вне threat model MVP
- zero-day malware, отсутствующий в сигнатурах и эвристиках выбранного AV;
- OCR изображений и semantic analysis PDF;
- password-protected/encrypted containers: в MVP они запрещаются, если содержимое нельзя полностью проверить;
- DLP/поиск персональных данных, токсичности и запрещённой тематики;
- переход по пользовательской ссылке и анализ удалённой страницы.
## 4. Общий pipeline
```mermaid
flowchart TD
postCheck[POST_check] --> auth[Auth_and_DTO]
auth --> kind{content_kind}
kind -->|text| normalizeText[Normalize_text]
normalizeText --> textRules[Text_rules]
textRules --> linkRules[Link_pipeline]
linkRules --> syncVerdict[200_or_403]
kind -->|file| metadata[Metadata_validation]
metadata --> cache{SHA256_cache}
cache -->|hit| cached[Sticky_200_or_403]
cache -->|miss| task[203_and_task]
task --> worker[File_worker]
worker --> objectRead[S3_stream_and_SHA256]
objectRead --> formatCheck[Format_validation]
formatCheck --> avScan[ClamAV_scan]
avScan --> finalVerdict[Persist_sticky_verdict]
finalVerdict --> taskGet[GET_task_200_or_403]
```
Приоритет:
1. service authentication, body/content-type/size и DTO validation;
2. idempotency/fingerprint conflict;
3. нормализация;
4. обязательные проверки для соответствующего `content_kind`;
5. любой deny имеет приоритет над allow;
6. инфраструктурная ошибка не превращается ни в allow, ни в domain deny.
## 5. Текстовый pipeline
### 5.1. Нормализация
Pipeline детерминирован:
1. принять только JSON UTF-8;
2. заменить `CRLF`/`CR` на `LF`;
3. Unicode normalization `NFKC`;
4. удалить leading Unicode whitespace;
5. сохранить исходный регистр и punctuation для rules;
6. ограничить текст internal DTO до 10 000 Unicode code points;
7. вычислить SHA-256 нормализованного текста для correlation/cache без хранения текста.
`content_kind=text` требует поле `text`; пустой текст отклоняется upstream `api-backend`. `content_kind=file` допускает пустой `text`; текстовые rules тогда не запускаются.
### 5.2. Rule engine
Rules поставляются как статический read-only bundle приложения. Динамический код, regex или rule definitions из запроса запрещены.
Каждое правило содержит:
- стабильный `rule_id`;
- `reason_code`;
- severity;
- scope (`text`, `url`, `file_metadata`);
- action (`deny`);
- `rules_version`;
- тестовые positive/negative cases.
Начальный rule pack MVP:
- `text.prompt_instruction_override` — конструкции вида «игнорируй предыдущие инструкции» и эквиваленты на поддерживаемых языках;
- `text.prompt_role_impersonation` — попытка обозначить пользовательский фрагмент как system/developer/tool instruction;
- `text.prompt_secret_extraction` — запрос раскрыть system prompt, credentials, tokens или внутренние инструкции;
Совпадение должно учитывать границы токенов и контекст, чтобы обычное обсуждение терминов не блокировалось простым substring match. Regex обязаны иметь ограниченную сложность и проходить ReDoS tests. Rule pack загружается и компилируется на startup; ошибка делает service not-ready.
Обычный текст без срабатывания обязательных rules получает `200 allow`. Неуверенное отсутствие совпадения не является deny. Ошибка rule engine является `500`, но не allow.
## 6. Pipeline ссылок
Сервис извлекает URL Unicode-aware parser-ом, а не одним regex.
Для каждой ссылки:
1. ограничить количество ссылок в сообщении и длину URL;
2. выполнить canonical parsing без автоматического исправления malformed URL;
3. разрешить только `http` и `https`;
4. запретить userinfo/credentials;
5. нормализовать IDNA host и отклонить malformed/confusable host;
6. для literal IP применить IP policy;
7. для hostname выполнить DNS resolve через доверенный resolver и проверить все A/AAAA;
Сервис не загружает содержимое URL и не следует redirects. Поэтому URL scan не создаёт исходящий HTTP SSRF. DNS failure или malformed URL, явно распознанный как ссылка, возвращает deny по policy; недоступность resolver для всех ссылок является dependency error.
Лимиты URL должны быть техническими env/settings, документированными в `arch-04` до реализации.
## 7. Файловый pipeline
### 7.1. Fast path POST
До создания задачи сервис:
- валидирует attachment DTO;
- проверяет допустимый MIME и `size_bytes`;
- проверяет checksum `sha256:<64 lowercase hex>`;
- проверяет, что `quarantine_object_key` соответствует разрешённому opaque key contract и не содержит traversal/control characters;
- ищет cache по `(sha256, scanner_engine, signatures_version, rules_version)`.
Явно недопустимые metadata дают sync `403 deny`. Cache hit возвращает sticky `200` или `403`. Cache miss создаёт задачу и возвращает `203 pending`.
Сервис не доверяет key как path и не строит произвольный URL. S3 client обращается только к configured quarantine bucket с virtual-hosted addressing и read-only credentials.
### 7.2. Worker
Worker:
1. атомарно claim-ит pending task с lease;
2. открывает S3 object stream с ограничением bytes/time;
3. вычисляет authoritative SHA-256;
4. сверяет фактический размер и checksum с DTO;
5. определяет реальный формат по содержимому;
6. сверяет detector result с declared MIME;
7. запрещает encrypted/password-protected и неподдерживаемые containers;
8. передаёт поток в `clamd` через internal network;
9. сохраняет sticky final verdict и audit;
10. записывает cache только для terminal результата;
11. завершает lease.
Чистый файл получает allow только если успешно завершились **все** обязательные проверки. `ClamAV FOUND`, checksum mismatch, format mismatch, unsupported encrypted content или policy limit дают deny с отдельным `rule_id`.
ClamAV timeout, protocol error, недоступность S3/DB/Redis или потеря lease не являются deny. Task остаётся pending/retryable в пределах deadline; после исчерпания retry получает terminal infrastructure failure, который API отдаёт как `503`, а не `403`.
### 7.3. AV runtime
MVP использует отдельный `clamd` sidecar/service в private Docker network:
- порт не публикуется наружу;
- сигнатуры обновляет `freshclam`;
- readiness требует daemon PING и допустимый возраст signatures;
- limits согласованы с максимальным размером файла;
- контейнер non-root, read-only root filesystem где возможно, отдельный writable volume только для signatures/runtime;
- worker не передаёт в clamd object key, имя пользователя или иные PII.
Недоступность AV переводит `/health/ready` в `503` и запрещает новые file allow.
## 8. Internal API
Все `/internal/safety/v1/*` доступны только `api-backend` в private network.
Headers:
```text
X-Service-Token: ${MESSAGE_SAFETY_SERVICE_TOKEN}
X-Request-ID: UUID/ULID (если нет — сервис создаёт)
X-Request-ID: UUID/ULID; при отсутствии генерируется сервисом
traceparent: optional W3C
Content-Type: application/json; charset=utf-8
```
Token сравнивается constant-time. Missing/invalid token → `401` или `403` internal auth error; выбран единый `401 service_unauthorized`, без подсказки о значении. Health не требует token внутри network либо использует отдельную ops policy.
Token сравнивается constant-time. Missing/invalid token → `401 service_unauthorized` без подсказок.
## 7. DTO `POST .../check`
Stub принимает минимальный versioned DTO, совместимый с потребностями api-backend:
### 8.1. POST `/internal/safety/v1/messages/check`
```json
{
"message_id": "uuid",
"content_kind": "text",
"text": "Фраза",
"text": "Текст сообщения",
"attachment": null
}
```
Для file:
```json
{
"message_id": "uuid",
@@ -104,162 +271,58 @@ Stub принимает минимальный versioned DTO, совместим
"quarantine_object_key": "opaque",
"mime_type": "application/pdf",
"size_bytes": 12345,
"checksum": "sha256:..."
"checksum": "sha256:<64-lowercase-hex>"
}
}
```
Неизвестные поля запрещены. `content_kind=text` требует text field (пустой разрешён именно stub default); `file` допускает attachment metadata, но не читает S3. `message_id` нужен для correlation/idempotency, не для выбора verdict.
Неизвестные поля запрещены. `text` и `file` — строгий discriminated union.
## 8. Ответы `POST .../check`
### `200 allow`
`200`:
```json
{
"verdict": "allow",
"rule_id": "stub.default_allow",
"rule_id": "safety.all_checks_passed",
"rules_version": "2026-01-01"
}
```
### `403 deny` для `ф/Ф`
`403`:
```json
{
"verdict": "deny",
"rule_id": "stub.starts_with_cyrillic_ef",
"reason_code": "stub_blocked",
"rule_id": "file.malware_detected",
"reason_code": "message_blocked",
"rules_version": "2026-01-01"
}
```
### `203 pending` для цифры
`203`:
```json
{
"verdict": "pending",
"task_id": "uuid",
"poll_after_ms": 2000,
"expires_at": "2026-07-10T12:15:00Z",
"expires_at": "2026-07-29T15:00:00Z",
"rules_version": "2026-01-01"
}
```
Все три — нормальные domain outcomes. `403` не участвует в circuit breaker failure count.
### 8.2. GET `/internal/safety/v1/messages/tasks/{task_id}`
## 9. Task storage Redis DB2
- running/retryable → `203 pending`;
- sticky allow → `200 allow`;
- sticky deny → `403 deny`;
- malformed UUID → `400 validation_error`;
- unknown/expired → `404 task_not_found`;
- dependency failure → `503`.
Ключ:
После final verdict повторный GET возвращает тот же HTTP status, verdict, `rule_id` и `rules_version`. Task не удаляется до retention expiry.
```text
han:safety:task:{task_id}
```
HASH/JSON v1:
```json
{
"schema_version": 1,
"message_id": "uuid",
"created_at_ms": 0,
"poll_count": 0,
"rng_context": "optional-test-only",
"rules_version": "2026-01-01"
}
```
TTL `MESSAGE_SAFETY_TASK_TTL_SEC`, default 900 seconds, должен быть больше `MESSAGE_SAFETY_TASK_POLL_MAX_SEC` (300) плюс network/recovery margin. Текст, attachment key и checksum в Redis не нужны. Создание task и TTL атомарны. Коллизия UUID повторяется bounded.
`message_id → task_id` dedup key допустим для идемпотентного повторного POST:
с тем же TTL; reserve обоих keys выполняется Lua. Повтор одинакового check возвращает тот же active task. Если fingerprint изменился для того же message id — `409 safety_request_conflict`.
## 10. `GET .../tasks/{task_id}`
Сначала проверяются token, UUID и существование task. Затем **на каждый GET независимо** выбирается один из трёх outcomes с вероятностью примерно 1/3:
- `203 pending`;
- `200 allow`;
- `400 stub_final_error` (terminal deny/error).
Предыдущий `200` или `400` не фиксируется как sticky verdict в Redis по буквальному требованию «дальнейший GET случайно и независимо». Следовательно, повторный GET того же task после terminal ответа теоретически может вернуть другой outcome. `api-backend` обязан прекратить polling на первом terminal `200/400`, поэтому противоречие снаружи не возникает.
Это поведение специально тестовое и не годится для production moderation. Для безопасной recovery production service должен сохранять sticky final verdict; переход потребует изменения режима/контракта.
"message":"Stub task returned a final negative verdict",
"request_id":"uuid",
"details":{"terminal":true}
}
}
```
Для malformed `task_id` используется `400 validation_error`, но его envelope имеет `verdict` отсутствующий и `details.terminal` отсутствует/false. Для неизвестного/expired task — `404 task_not_found`. Api-backend различает terminal stub `400` строго по schema/code, а не по одному HTTP status.
## 11. Worker/poll model
Реальный worker не требуется. Task создаётся сразу, а GET эмулирует состояние worker случайным outcome. Контракт остаётся таким же, как для async orchestration: check создаёт `task_id`, api-backend poll-ит GET внутри исходного user POST.
Опциональный `SAFETY_STUB_WORKER_MODE=emulated_on_poll` — единственный режим MVP. Будущий worker mode не должен менять endpoint/DTO, но final verdict тогда становится sticky.
Api-backend:
```text
POST check
200 -> allow
403 -> deny -> public 422 message_blocked
203 -> poll GET
GET 203 -> continue
GET 200 -> allow
GET 400 + code=stub_final_error + terminal=true
-> deny -> public 422 message_blocked
other 400 -> dependency contract error, not message verdict
timeout/5xx/redis unavailable -> public 503/504
```
## 12. Randomness и deterministic testing
Production-like stub default использует криптографически достаточный process RNG либо `random.Random`с entropy seed; распределение не является security decision.
RNG внедряется через интерфейс `VerdictRng.choice()`. Test implementations:
- sequence RNG: `pending, allow, final_error`;
- seeded RNG через `SAFETY_STUB_RNG_SEED` только при `APP_ENV=test`;
- forced outcome через dependency override, не public header.
В production-like env seed/forced mode вызывает startup failure, чтобы внешний caller не управлял verdict. Статистический test на большой выборке проверяет каждую долю в допустимом диапазоне (например, 0.30–0.36), но основные tests используют sequence RNG и не flaky.
«Независимо» означает новый RNG draw на каждый валидный GET; poll count/предыдущий outcome не влияют на draw.
## 13. Error semantics
Internal envelope:
### 8.3. Error envelope
```json
{
@@ -272,187 +335,293 @@ Internal envelope:
}
```
| HTTP | Code | Retry/смысл |
| HTTP | Code/смысл | Retry |
|---|---|---|
| 400 | `validation_error` | malformed, не terminal verdict |
| `429` | `rate_limit_exceeded` | по `Retry-After` |
| `500` | `internal_error` | да |
| `503` | dependency/scanner/storage unavailable | да |
Domain `403` и terminal stub `400` не считаются infrastructure failure circuit breaker.
Domain `403` не считается circuit breaker failure.
## 14. Idempotency и concurrency
## 9. Idempotency и state model
POST fingerprint = SHA-256 canonical normalized DTO без request-id/token. Lua reserve обеспечивает один task на `(message_id,fingerprint)` в TTL. Concurrent duplicate получает тот же task id.
Fingerprint = SHA-256 canonical normalized DTO без token/request-id/trace headers.
GET атомарно проверяет существование и увеличивает `poll_count`; RNG draw выполняется независимо. Удалять task после terminal нельзя, иначе повтор получил бы 404 и нарушил независимый test behavior. TTL выполняет cleanup.
- одинаковый `(message_id, fingerprint)` возвращает тот же активный task или sticky final verdict;
- тот же `message_id`с другим fingerprint → `409 safety_request_conflict`;
- concurrent duplicate создаёт одну task;
- final verdict изменять запрещено;
- transient error не кэшируется как allow/deny;
- повторный worker delivery безопасен через task state compare-and-set.
## 15. Health
Task states:
`GET /health/live`: только process/event loop, всегда без Redis call.
`allowed`, `denied`, `failed` terminal и sticky. `failed` не является safety deny.
- env/token/rules version валидны;
- Redis DB2 auth, PING и короткий SET/GET/DEL с TTL;
- RNG provider доступен;
- OpenAPI schema загружена.
## 10. Хранение данных
Redis down → `503 {"status":"not_ready","components":{"redis":"down"}}`. Текстовые sync rules технически вычислимы, но service целиком not-ready, а digit check возвращает 503, чтобы не выдавать task без storage.
- task status, attempts, lease owner/until, next attempt/deadline;
- declared MIME/size/checksum;
- verdict, `rule_id`, `reason_code`;
- rules/scanner/signatures versions;
- timestamps и common audit fields.
Не логируются service token, message text, attachment key/name, checksum, DTO body или PII. Разрешены message/task UUID при принятой retention либо их hash.
Raw message text, file bytes, object key, filename, service token и AV stream в PostgreSQL не сохраняются. Для correlation используются UUID и cryptographic hashes.
### 10.2. Redis DB2
Redis используется для:
- short-lived task lookup;
- dedup/reservation coordination;
- hot verdict cache;
- leases/locks;
- internal rate limits.
PostgreSQL остаётся durable source of truth. Потеря Redis не должна менять sticky final verdict; сервис восстанавливает state из PostgreSQL. Redis unavailable делает service not-ready и file task creation недоступным.
TTL task должен превышать `MESSAGE_SAFETY_TASK_POLL_MAX_SEC` плюс recovery/network margin. Verdict cache TTL задаётся отдельно и инвалидируется версиями rules/scanner/signatures.
## 11. Согласование с `api-backend`
```text
POST check
200 -> allow -> text accepted or file promote -> Bitrix delivery
403 -> blocked/rejected -> no Bitrix -> public 422 message_blocked
203 -> persist han_app.safety_tasks -> poll GET
GET 203 -> continue
GET 200 -> allow branch
GET 403 -> deny branch
timeout/5xx -> public 503/504, no promote, no Bitrix
```
`message-safety` не перемещает и не удаляет S3 object. На deny это идемпотентно делает `api-backend`. Клиент не получает internal `203`.
## 12. Security controls
- internal network only; endpoint не публикуется через nginx и host port;
- unique service token только из env/secret mount;
- strict DTO, body/text/URL/file limits и запрет unknown fields;
- read-only S3-quarantine access, без list/write/delete;
- no arbitrary URL fetch, no redirect following;
- egress allow-list только DNS, S3, PostgreSQL, Redis, OTLP и clamd по назначению;
- parameterized SQL и least-privilege DB role только на schema `message_safety`;
Запрещено логировать message text, extracted URL целиком, file bytes, object key/name, checksum целиком, DTO body и service token. URL host и hash допустимы только при утверждённой retention policy; по умолчанию логируется category/hash.
Metrics:
- requests/latency/errors по route/status;
- check outcomes allow/deny/pending;
- task GET outcomes pending/allow/final_error;
- observed distribution;
- task create/dedup/conflict/not-found/expired;
- Redis latency/error/pool;
- auth rejects, rate limit;
- RNG mode как low-cardinality info;
- readiness.
- request rate/latency/status по route;
- checks/verdicts по `content_kind`, `allow|deny|pending|error`;
Новые env (`TASK_TTL`, `POLL_AFTER`, stub mode/seed) требуют внесения в arch-04 перед реализацией production config; здесь они зафиксированы как предложение.
ClamAV endpoint, signature max age, worker concurrency, retry/lease, task/cache TTL, text/URL limits и detector version требуют добавления в `arch-04` до реализации. Бизнес allow-list типов/размера остаётся в `app_settings`; `api-backend` передаёт согласованные metadata, а safety использует versioned runtime snapshot.
## 19. OpenAPI
Startup отклоняет placeholders, insecure production defaults, несовместимые timeout/TTL и отсутствующие обязательные параметры.
`message-safety/openapi.yaml` OpenAPI 3.1 обязателен и включает:
## 16. OpenAPI
- security scheme `X-Service-Token`;
- check request union text/file;
- exact 200/203/403 responses POST;
- exact 200/203/400/404 responses GET;
- discriminator между malformed 400 и terminal stub 400;
`message-safety/openapi.yaml` OpenAPI 3.1 обязателен и содержит:
- `X-Service-Token` security scheme;
- common request/trace headers;
- examples, max lengths, UUID/checksum formats;
- health endpoints.
- strict discriminated union text/file;
- exact POST responses `200/203/400/401/403/409/429/500/503`;
- exact GET responses `200/203/400/401/403/404/429/500/503`;
- domain deny отдельно от error envelope;
- UUID/checksum/max length examples;
- health contracts.
Generated/runtime schema сравнивается с committed artifact. Contract test api-backend отдельно закрепляет mapping terminal `400 stub_final_error → 422 message_blocked`.
- blocked/failed content не попадает в Bitrix и не promote-ится;
- health/readiness отражает PG/Redis/S3/workers/ClamAV/rules;
- OpenAPI 3.1 и runtime parity зелёные;
- logs/metrics/traces не содержат содержимое сообщений, файлов и secrets;
- hardened containers запускаются без public port;
- runbook описывает signature update, stale signatures, AV outage, retry и rollback.
## 22. Решения, допущения и TBD
## 19. Переход с текущей заглушки и follow-up
**Решения:** normalizer NFKC+lstrip; Unicode `Nd`; default allow; emulation on GET без worker; independent non-sticky outcomes; `400 stub_final_error` terminal и преобразуется API в 422.
Текущая реализация в `codebase/backend/message-safety/` остаётся test stub и **не соответствует** этой production-спецификации. Для перехода отдельной задачей необходимо:
**Допущения:** пустой/file-only text попадает в default 200; `message_id` передаётся internal DTO; Redis task TTL 900 секунд достаточен для MVP tests.
1. удалить правила `ф/Ф`, digit task и random RNG;
2. удалить terminal `400 stub_final_error`;
3. реализовать sticky canonical `403` для async deny;
5. добавить migrations, полный OpenAPI и тестовую матрицу;
6. синхронизировать `module-01-api-backend.md` и код api-backend, удалив test-only mapping `stub_final_error`;
7. синхронизировать `module-09-observability.md` и dashboards, удалив stub distribution panels/маркировку;
8. добавить новые technical env и ClamAV deployment contract в `arch-04`/`arch-03` до production реализации.
**TBD:**
До выполнения перехода контейнер должен быть явно маркирован как stub и не считаться production security control.
- S1 формально обновить arch-02 для test-only terminal 400 или вернуть production 403;
- S2 окончательный internal DTO/fingerprint в OpenAPI;
- S3 добавить новые env в arch-04;
- S4 точный Redis task TTL относительно extended recovery module-01;
- S5 sticky final verdict при переходе от stub к реальному Safety;
- S6 реальные file/link checks, PostgreSQL schema и S3 read-only — вне scope заглушки.
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.