Files
han-app/modules/module-05-message-safety.md
T

628 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# module-05. Проектная спецификация `message-safety`
> Статус: целевая production-спецификация MVP.
> Канонические источники: [`README.md`](../architectory/README.md), [`arch-00-glossary.md`](../architectory/arch-00-glossary.md), [`arch-01-system-architecture.md`](../architectory/arch-01-system-architecture.md), [`arch-02-api-contracts.md`](../architectory/arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](../architectory/arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](../architectory/arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](../architectory/arch-05-agent-development-process.md), [`module-01-api-backend.md`](module-01-api-backend.md).
## 1. Назначение и приоритет
`message-safety` — внутренний сервис, который до отправки сообщения в Bitrix24 проверяет пользовательский текст, содержащиеся в нём ссылки и файлы из S3-quarantine.
Сервис закрывает угрозы, поступающие через пользовательское сообщение:
- управляющие и prompt-injection конструкции, направленные на оператора или последующую автоматическую обработку;
- опасные URL-схемы, URL с credentials и ссылки на private/link-local/metadata адреса;
- HTML/script-like payloads, способные стать активным содержимым при небезопасном отображении;
- подмену типа файла, несоответствие заявленного MIME фактическому формату и checksum;
- вредоносные файлы, обнаруживаемые антивирусными сигнатурами.
Спецификация детализирует архитектуру, но не меняет её. При конфликте приоритет имеют `arch-00``arch-05`. Канонические domain outcomes:
- `200 allow`;
- `403 deny`;
- `203 pending` с последующим sticky `200` или `403`.
Test-only правила по первому символу, случайные verdict и terminal `400 stub_final_error` в production-контракт не входят.
## 2. Границы ответственности
### 2.1. Сервис отвечает за
- строгую валидацию 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-логи.
### 2.2. Сервис не отвечает за
- JWT пользователя, согласия и авторизацию доступа пользователя к диалогу;
- edge/API rate limits;
- загрузку файла и выдачу presigned URL;
- запись пользовательского сообщения и статусов в `han_app`;
- copy/promote файла из quarantine в S3-data и удаление объекта;
- доставку в Bitrix24, realtime и пользовательский текст ошибки;
- анализ входящих сообщений оператора;
- ML-модерацию смысла, токсичности или правдивости текста.
Этими операциями владеет `api-backend` или соответствующий архитектурный модуль.
## 3. Threat model MVP
### 3.1. Текст и ссылки
| Угроза | Контроль | Результат |
|---|---|---|
| Prompt/control injection | Версионированные Unicode-aware rules | `403 deny` |
| Попытка выдать текст за system/developer instruction | Нормализация + rule pack | `403 deny` |
| Script/active-content payload | Правила для script, event-handler и опасных embedding-конструкций | `403 deny` |
| Опасная URL-схема | Разрешены только `http` и `https` для распознанных web URL | `403 deny` |
| URL с userinfo/credentials | Запрет `user:password@host` | `403 deny` |
| SSRF-ссылка | DNS/IP classification, запрет private, loopback, link-local, multicast, unspecified и metadata endpoints | `403 deny` |
| Обход Unicode/whitespace | NFKC, CRLF→LF, Unicode whitespace handling | Проверка нормализованного текста |
| ReDoS/DoS правилами | Линейные/ограниченные regex, лимиты текста, URL и времени | `400` или dependency error |
Rules не заменяют безопасный rendering. Frontend и Bitrix integration обязаны экранировать текст; safety является дополнительным барьером, а не HTML sanitizer.
### 3.2. Файлы
| Угроза | Контроль | Результат |
|---|---|---|
| Недопустимый размер/MIME | Сверка DTO с allow-list и лимитами | `403 deny` |
| Подмена MIME | Magic-byte/content sniffing, сверка declared MIME | `403 deny` |
| Подмена содержимого после complete | Полный SHA-256 против DTO checksum | `403 deny` |
| Malware | ClamAV scan актуальными сигнатурами | `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 или внутренние инструкции;
- `text.active_script``<script>`, inline event handlers и эквивалентные active-content шаблоны;
- `url.forbidden_scheme`;
- `url.credentials_present`;
- `url.private_destination`.
Совпадение должно учитывать границы токенов и контекст, чтобы обычное обсуждение терминов не блокировалось простым 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;
8. запретить loopback, RFC1918/ULA, link-local, multicast, unspecified, reserved ranges и cloud metadata endpoints.
Сервис не загружает содержимое 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; при отсутствии генерируется сервисом
traceparent: optional W3C
Content-Type: application/json; charset=utf-8
```
Token сравнивается constant-time. Missing/invalid token → `401 service_unauthorized` без подсказок.
### 8.1. POST `/internal/safety/v1/messages/check`
```json
{
"message_id": "uuid",
"content_kind": "text",
"text": "Текст сообщения",
"attachment": null
}
```
```json
{
"message_id": "uuid",
"content_kind": "file",
"text": "",
"attachment": {
"attachment_id": "uuid",
"quarantine_object_key": "opaque",
"mime_type": "application/pdf",
"size_bytes": 12345,
"checksum": "sha256:<64-lowercase-hex>"
}
}
```
Неизвестные поля запрещены. `text` и `file` — строгий discriminated union.
`200`:
```json
{
"verdict": "allow",
"rule_id": "safety.all_checks_passed",
"rules_version": "2026-01-01"
}
```
`403`:
```json
{
"verdict": "deny",
"rule_id": "file.malware_detected",
"reason_code": "message_blocked",
"rules_version": "2026-01-01"
}
```
`203`:
```json
{
"verdict": "pending",
"task_id": "uuid",
"poll_after_ms": 2000,
"expires_at": "2026-07-29T15:00:00Z",
"rules_version": "2026-01-01"
}
```
### 8.2. GET `/internal/safety/v1/messages/tasks/{task_id}`
- 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.
### 8.3. Error envelope
```json
{
"error": {
"code": "validation_error",
"message": "Request is invalid",
"request_id": "uuid",
"details": {}
}
}
```
| HTTP | Code/смысл | Retry |
|---|---|---|
| `400` | malformed DTO/path | нет без исправления |
| `401` | `service_unauthorized` | нет без исправления secret |
| `403` | domain `deny` | нет |
| `404` | `task_not_found` | dependency reconciliation |
| `409` | `safety_request_conflict` | нет |
| `429` | `rate_limit_exceeded` | по `Retry-After` |
| `500` | `internal_error` | да |
| `503` | dependency/scanner/storage unavailable | да |
Domain `403` не считается circuit breaker failure.
## 9. Idempotency и state model
Fingerprint = SHA-256 canonical normalized DTO без token/request-id/trace headers.
- одинаковый `(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.
Task states:
```text
pending -> processing -> allowed
-> denied
processing -> pending (retry with lease expiry)
processing -> failed (infrastructure retries exhausted)
```
`allowed`, `denied`, `failed` terminal и sticky. `failed` не является safety deny.
## 10. Хранение данных
### 10.1. PostgreSQL schema `message_safety`
`safety_tasks`:
- `id`, `message_id`, `attachment_id`;
- request fingerprint и content hash;
- 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.
`verdict_cache`:
- content SHA-256;
- rules/scanner/signatures versions;
- sticky verdict и rule;
- expiry/created timestamps;
- unique versioned cache key.
`safety_audit`:
- request/task identifiers;
- event (`received`, `task_created`, `rule_hit`, `scan_completed`, `dependency_failed`);
- verdict/rule/version;
- duration and technical error category;
- timestamp.
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`;
- Redis ACL только DB2 и prefixes `han:safety:*`;
- dependency pinning, SBOM/image scanning;
- non-root, read-only root fs, tmpfs `/tmp`, dropped capabilities, no-new-privileges;
- OpenAPI UI выключен в production;
- безопасные generic errors без stack/internal addresses;
- правила и AV signatures обновляются только trusted deployment process.
## 13. Observability и privacy
JSON logs:
- `timestamp`, `level`, `service.name=message-safety`, `module`, `event`;
- `request_id`, `trace_id`, `span_id`, route, status, duration;
- verdict, `rule_id`, rules/scanner/signatures version;
- task state, attempt, age/size bucket;
- dependency/error code.
Запрещено логировать message text, extracted URL целиком, file bytes, object key/name, checksum целиком, DTO body и service token. URL host и hash допустимы только при утверждённой retention policy; по умолчанию логируется category/hash.
Metrics:
- request rate/latency/status по route;
- checks/verdicts по `content_kind`, `allow|deny|pending|error`;
- rule hits по low-cardinality `rule_id`;
- task queue/age/attempts/lease conflicts/timeouts;
- scan latency/bytes buckets/AV outcome;
- signatures age/version info;
- cache hit/miss;
- PostgreSQL/Redis/S3/ClamAV latency and errors;
- auth rejects/rate limit/readiness.
Telemetry collector unavailable не влияет на safety verdict и readiness.
## 14. Health
`GET /health/live` проверяет только process/event loop.
`GET /health/ready` проверяет:
- settings, secret и rules bundle;
- PostgreSQL read/write в schema `message_safety`;
- Redis DB2 PING и prefixed SET/GET/DEL;
- worker heartbeat/lease processing;
- S3-quarantine Head/Get read permission на безопасный canary object;
- ClamAV PING и допустимый возраст signatures;
- OpenAPI schema availability.
Критическая dependency down → `503`:
```json
{
"status": "not_ready",
"components": {
"postgres": "ok",
"redis": "ok",
"s3_quarantine": "ok",
"worker": "ok",
"antivirus": "down",
"rules": "ok"
}
}
```
Health не требует service token внутри private ops network и не раскрывает credentials/hostnames.
## 15. Configuration
Канонические существующие env:
```text
APP_ENV=production-like
MESSAGE_SAFETY_DATABASE_URL=postgresql://message_safety_app:<secret>@<host>:5433/han_chat
MESSAGE_SAFETY_REDIS_URL=redis://message_safety:<secret>@redis:6379/2
MESSAGE_SAFETY_SERVICE_TOKEN=<secret>
MESSAGE_SAFETY_RULES_VERSION=2026-01-01
MESSAGE_SAFETY_POST_TIMEOUT_SEC=5
MESSAGE_SAFETY_TASK_POLL_INTERVAL_SEC=2
MESSAGE_SAFETY_TASK_POLL_MAX_SEC=300
MESSAGE_SAFETY_FILE_SCAN_TIMEOUT_SEC=60
SELECTEL_S3_ENDPOINT_URL=https://s3.storage.selcloud.ru
SELECTEL_S3_BUCKET_QUARANTINE=han-chat-quarantine
SELECTEL_S3_QUARANTINE_READ_ACCESS_KEY=<secret>
SELECTEL_S3_QUARANTINE_READ_SECRET_KEY=<secret>
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
```
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.
Startup отклоняет placeholders, insecure production defaults, несовместимые timeout/TTL и отсутствующие обязательные параметры.
## 16. OpenAPI
`message-safety/openapi.yaml` OpenAPI 3.1 обязателен и содержит:
- `X-Service-Token` security scheme;
- common request/trace headers;
- 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.
Runtime schema сравнивается с committed artifact contract test. Terminal `400 stub_final_error` отсутствует.
## 17. Тестовая матрица
### Unit
- NFKC, CRLF, Unicode whitespace, max length;
- positive/negative cases каждого text rule;
- границы токенов и false-positive corpus;
- ReDoS/time budget всех regex;
- URL parsing, IDNA, schemes, credentials и IP ranges IPv4/IPv6;
- DTO union, fingerprint и rule priority;
- MIME/magic/checksum decision table;
- sticky state transitions;
- log redaction.
### Integration
- PostgreSQL migration/constraints/idempotency/audit;
- Redis DB2 reserve/cache/TTL/lease concurrency/recovery;
- S3 read-only stream, object changed/missing/timeout;
- ClamAV clean, EICAR, FOUND, timeout, daemon down, stale signatures;
- full SHA-256 and size mismatch;
- duplicate concurrent request and worker retry;
- dependency recovery without changed final verdict.
### Contract
- text allow and each deny category;
- file cache hit, `203` then sticky `200`, `203` then sticky `403`;
- no random/non-sticky outcomes;
- malformed `400` never treated as deny;
- auth missing/wrong/correct;
- request-id and trace propagation;
- api-backend maps only domain `403` to public `422 message_blocked`;
- deny/timeout never calls Bitrix and never promotes quarantine;
- OpenAPI runtime parity.
### Security/failure
- EICAR and safe corpus;
- malformed/polyglot/encrypted files;
- decompression and parser resource limits;
- URL private/link-local/metadata/IPv4-mapped-IPv6 cases;
- DNS rebinding simulation;
- no raw text/token/URL/object key/checksum in logs/traces/errors;
- S3 credentials cannot list/write/delete;
- Redis ACL and PostgreSQL schema isolation;
- telemetry outage does not affect verdict.
## 18. Definition of Done
- production `200/203/403` contract реализован без stub divergence;
- text rules и URL pipeline покрывают threat model и false-positive corpus;
- files проходят metadata, authoritative SHA-256, format detector и ClamAV;
- final task verdict sticky и durable;
- idempotency/concurrency/recovery доказаны тестами;
- PG schema `message_safety`, Redis DB2 и S3 read-only работают по least privilege;
- cache versioned rules/scanner/signatures и не сохраняет transient errors;
- api-backend mapping allow/deny/timeout проверен end-to-end;
- 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.
## 19. Переход с текущей заглушки и follow-up
Текущая реализация в `codebase/backend/message-safety/` остаётся test stub и **не соответствует** этой production-спецификации. Для перехода отдельной задачей необходимо:
1. удалить правила `ф/Ф`, digit task и random RNG;
2. удалить terminal `400 stub_final_error`;
3. реализовать sticky canonical `403` для async deny;
4. подключить PostgreSQL schema `message_safety`, Redis DB2, S3 read-only и ClamAV workers;
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 реализации.
До выполнения перехода контейнер должен быть явно маркирован как stub и не считаться production security control.