1
План stage7 saas
stepan edited this page 2026-09-13 00:17:00 +03:00
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.

Перенесено из репозитория (docs/superpowers/plans/2026-09-05-deal-stage7-saas.md). Актуальная версия — здесь, в вики.

Дейл (Deal) — Этап 7: SaaS-контур (оператор, инвайты, лимиты, аудит, безопасность, prod-деплой, финальные доки) Implementation Plan

Исторический документ этапа 7. Актуальное состояние — docs/superpowers/STATUS.md и docs/technical/Техническая-документация-Дейл.md.

Goal: Замкнуть SaaS-контур «Дейла» поверх готового мультитенантного ядра этапов 0–6: отдельный изолированный контур оператора (вход, тенанты, инвайты, лимиты/бюджеты, health, impersonation, чтение аудита) в public-схеме и на новых REST-ручках /api/operator/* + /api/join (активация инвайта); бюджет токенов на тенанта с автоматическим fallback на ML/локальный разбор и уведомлением (приём не блокируется); аудит-поток (входы, инвайты, impersonation, действия оператора — append-only); безопасность: лимит попыток входа, rate limiting (приложение + gRPC- ингресс), Origin-проверка мутаций, security-заголовки, mTLS за флагом для внутренних сервисов; prod-деплой: deploy/compose.prod.yml (postgres, minio, core, 3 сервиса, Caddy, grafana/loki/ promtail) + ежедневные бэкапы; observability: Serilog (JSON-логи в core и сервисах) → Promtail → Loki → Grafana; финальные доки (техдок §11/§13, roadmap, STATUS, user-guide, api-map-дополнение) и сквозная SaaS-приёмка. Фронт Vue не переписывается: операторская админка — API-only (UI — вне).

Architecture: все SaaS-сущности живут в public (системная схема), владелец — существующий модуль Deal.Modules.Tenants (дизайн-док §5 L128: «тенанты, пользователи, инвайты, лимиты, аудит, операторская админка»), EF-адаптеры — в Deal.Infrastructure, HTTP — в Deal.Api/Endpoints. Оператор — НЕ тенант: отдельные таблицы Operators/OperatorSessions, отдельная кука deal_operator_session, отдельный bootstrap из env. Тенант-сессия остаётся как есть (deal_session, SessionMiddleware). Активация инвайта создаёт пользователя + тенанта (при необходимости) и провижинит схему существующим TenantService/ITenantProvisioner. Учёт токенов ИИ, который этап 6 копил в tenant-KV (SettingsKeys.AiTokenUsage, AiUsageLedger), на этапе 7 пишется в public.tenant_limits (период + UsedTokens, ленивый reset) — это источник истины для бюджетного гейта; KV-ключ остаётся как «lifetime»-счётчик. Гейт ставится НЕ внутрь ai-service, а в core на границе вызова ИИ (декораторы IAiClassifier/IAiTools с fallback на Local-реализации — ровно семантика «aiEnabled=false/aiFail» этапов 4–6), поэтому контракты/сервисы этапа 6 не меняются. Rate limiting — встроенный AddRateLimiter ASP.NET Core + прикладной LoginAttemptGuard; mTLS — за флагом (dev остаётся plaintext + service-token). Observability: Serilog JSON во всех процессах, сбор логов контейнеров Promtail → Loki → Grafana (compose-prod); OTel-метрики задекларированы follow-up (минимум-объём).

Tech Stack: .NET 10, существующие порты/паттерны этапов 1–6; новые пакеты в core: Serilog, Serilog.Sinks.Console, Serilog.Sinks.File, Grpc.HealthCheck (клиент health для операторского health-эндпоинта). Rate limiting — shared-framework (System.Threading.RateLimiting/AddRateLimiter, новый NuGet не нужен). Инфраструктурные файлы (не код): deploy/compose.prod.yml, deploy/caddy/ Caddyfile, deploy/observability/{promtail.yml,loki.yml,grafana-provisioning/*}, deploy/.env.prod. example, scripts/mtls-certs.sh, scripts/backup.sh. Docker-движок в ходе этапа может быть выключен: все acceptance-задачи — без docker там, где можно; «живые» шаги явно помечены ⚠ Manual.

Spec: docs/architecture/2026-09-05-deal-architecture-design.md §8 (безопасность, L187216), §9 (observability/админка/бэкапы, L216–233), §10 (деплой, L233243), §12.5; docs/spec/ ТЗ-дейл-новая-архитектура.md §3 (роли), §9 (лимиты), §10 (админка), §11 (НФТ); решения владельца в docs/superpowers/plans/2026-09-05-deal-roadmap.md (L107–121 + «Выполнено» этапов 1–6 + «Оставшиеся этапы» L101–105); ограничения этапа 6 (roadmap L8284, STATUS.md); текущий код: Deal.Modules.Tenants (AuthService/TenantService/порты), Deal.Infrastructure (миграции/конфигурации/репозитории), Deal.Api (Program.cs, SessionMiddleware, AuthEndpoints, хостинг-циклы, SseBroker), AiUsageLedger

  • GrpcAiClassifier/GrpcAiTools, PipelineWorkerService (ветки aiEnabled/fallback), deploy/ compose.dev.yml, техдок §8–§11/§13, api-map §3.9/§5.

Global Constraints

  • Проект НЕ git; фиксация — отчёты task-N-report.md и progress.md в .superpowers/sdd/deal-stage7-saas/.
  • .NET 10; все sln собираются 0 warnings/0 errors (TreatWarningsAsErrors); dev-Postgres deal-postgres (:5433); системные миграции применяются командой dotnet ef database update --context DealDbContext (из src/core), tenant-миграции — провижинером на старте (не меняется).
  • Код-стайл этапов 1–6: 1 тип = 1 файл; XML-doc на public; комментарии на русском; без регионов; без магических чисел (именованные константы); времена DateTimeOffset (UTC); JSON camelCase; ошибки API — {detail}; кука httpOnly/SameSite=Lax.
  • Vue-фронт, backend/, mlservice/ (python), корневой docker-compose.ymlне трогаем. Новые SaaS-ручки — дополнение к /api (фронт их не вызывает); контракт api-map для фронта не ломается.
  • Секреты — только env/файлы (DEAL_*), никогда в коде/БД в открытом виде; в аудит и логи секреты не пишутся.
  • Все SaaS-таблицы — public; TenantDbContext/схемы тенантов не меняются (кроме случаев, когда требуется новое tenant-поле, — в этапе 7 таких нет).
  • Креды оператора/инвайт-коды в тестах и примерах — фиксированные dev-значения; живые проверки (Docker-стек, mTLS-рукопожатие, бэкап-прогон) — ⚠ Manual, по возможности.

Зафиксированные решения (Rulings этапа)

Сокращения путей: TM= src/core/Deal.Modules.Tenants/, I= src/core/Deal.Infrastructure/, A= src/core/Deal.Api/, C= src/core/Deal.Contracts/, ST= src/core/Deal.Modules.Settings/, PL= src/core/Deal.Modules.Pipeline/, T= src/core/tests/Deal.Tests.Unit/, DEP= deploy/compose.dev.yml, PROD= deploy/compose.prod.yml, TG= src/telegram-service/, AI= src/ai-service/, ML= src/ml-service/.

  • Ruling 1 (а) — модель оператора/сессий: public-таблицы, изоляция, bootstrap. Новые таблицы public (системная миграция SystemSaaS, команда EF как в техдок §13.2): Operators (Id Guid PK, Login unique (нижний регистр), PasswordHash Argon2id, Status, CreatedAt), OperatorSessions (TokenHash PK, OperatorId FK→Operators, Login, ExpiresAt, CreatedAt; срок жизни 12 часов), Invites, TenantLimits, AuditLog (Rulings 4/5/8). Сущности/конфигурации — по образцу TenantEntity/UserEntity/SessionConfiguration (ToTable в public, нижний регистр имён). Кука оператора — deal_operator_session (отдельная от тенантной deal_session; httpOnly, SameSite=Lax, Secure из конфига, секция OperatorCookies). Операторская сессия разрешается отдельным OperatorSessionMiddleware (после SessionMiddleware) в HttpContext.Items["CurrentOperator"]; эндпоинты /api/operator/* требуют именно операторскую сессию (403/401), тенантные /api-ручки её не видят (другое имя куки — взаимной подмены нет). Bootstrap оператора: env DEAL_OPERATOR_LOGIN/DEAL_OPERATOR_PASSWORD; в Development при их отсутствии — дефолт operator/operator (зеркало dev-seed admin/admin). В Production при отсутствии кред — стартовый warning и пропуск (оператор заводится позже через env + рестарт; кода регистрации оператора нет). Dev-seed дефолтного тенанта/admin/admin становится dev-only: TenantBootstrapService создаёт дефолтного тенанта только в Development или при DEAL_BOOTSTRAP_DEFAULT_TENANT=1; провижининг схем всех зарегистрированных тенантов выполняется всегда. Прод-тенантов заводит оператор.
  • Ruling 2 (б) — инвайты и активация. Invites (public): Code PK (случайный url-safe, 16 симв., префикса нет), TenantId Guid nullable (null = «новый тенант»), Email (нормализованный, unique по активным), Status (pending/activated/revoked/expired), ExpiresAt (72 ч, константа), CreatedById (оператор), ActivatedAt null, CreatedAt. Создание/отзыв — только оператор. Активация — публичная ручка POST /api/join {code, email, name?, password}: email обязан совпасть с инвайтом; проверка статуса/expiry (expired → 410-семантика текстом «Срок действия приглашения истёк»); пароль ≥4 (как в AuthService); создание пользователя (login=email, Argon2id) и, если TenantId пуст, тенанта (TenantService.CreateTenantAsync(name, newId) — провижинит схему сам); отметка activated + аудит. Глобальная уникальность email обеспечена unique-индексом users.login (конфликт → 400 «Этот email уже зарегистрирован»). Инвайт на существующего тенанта (TenantId задан) создаёт пользователя в нём. Отдельной страницы-активации во фронте нет — ручка API-only (curl/будущий UI); в user-guide фиксируется описание.
  • Ruling 3 (в) — лимиты: модель, период, списание, гейт, fallback, уведомление. Таблица TenantLimits (public): TenantId PK (FK→tenants, Restrict), BudgetTokens bigint, Period (month|day, default month), PeriodStart, UsedTokens bigint (с начала периода), Warned80 bool, NotifiedExhausted bool, UpdatedAt. Списание: там, где этап 6 звал AiUsageLedger.AddAsync (GrpcAiClassifier/GrpcAiTools, успешные RPC ai-service), новый TokenUsageRecorder.AddAsync пишет (1) инкремент UsedTokens в tenant_limits (тот же scoped DealDbContext) и (2) по-прежнему lifetime-сумму в KV aiTokenUsage (существующий ключ — счётчик «всего», оператор/будущий UI). Reset — ленивый: при чтении/записи, если сейчас ≥ конца периода (PeriodStart+месяц/сутки), UsedTokens/флаги обнуляются и PeriodStart=now; отдельного фонового цикла нет. Гейт — порт ITokenBudgetGate.CheckAsync(tenantId){Allowed, Exceeded, Status}; статус тенанта (suspended) трактуется как Not Allowed (приостановка замораживает ИИ). Гейт спрашивают декораторы BudgetedAiClassifier/BudgetedAiTools (регистрируются в AddDealIntegrations, только когда Services:Ai:UseLocal=false, поверх gRPC-адаптеров): исчерпано → фильтр/классификация через Local-реализации (семантика aiEnabled=false / aiFail), IAiTools.EvaluateFit → исключение AiUnavailableException (Discovery-воркер сам уходит в эвристику — код не меняется), GenerateKeywords → мягкая ошибка {keywords:[], error}. Уведомление: пороги 80% и 100% от бюджета; обнаружение перехода и публикация SSE-тоста («ИИ-бюджет израсходован на 80%» / «ИИ-бюджет исчерпан — обработка в локальном режиме», иконка bell) — Api-хостинг BudgetAlertScheduler (60 с, эталон StorageTickScheduler), флаги Warned80/NotifiedExhausted гарантируют один тост на период на порог; смена бюджета оператором сбрасывает флаги. Приём и базовая обработка сообщений не блокируются (fallback по замыслу ТЗ §9). Дефолт-бюджет нового тенанта — константа модуля TokenBudgetDefaults (10 000 000 токенов/месяц), оператор задаёт бюджет при создании или меняет позже.
  • Ruling 4 (г) — аудит: append-only поток. Таблица AuditLog (public): Id bigint identity PK, At, ActorType (operator|tenant|system), ActorId Guid null, TenantId Guid null, EventType (строковая константа), Ip string null, DetailJson (JSON, без секретов). События (каталог AuditEvents): tenant_login_ok, tenant_login_failed, operator_login_ok, operator_login_failed, invite_created, invite_revoked, invite_activated, tenant_created, tenant_status_changed, tenant_limit_changed, impersonation_started. Пишет только AuditService (модуль Tenants, порт IAuditLogStore → адаптер AuditLogStore), вызывается из эндпоинтов/сервисов; UPDATE/DELETE в приложении отсутствуют (append-only на уровне кода и конвенции; DB-триггеры не добавляем). Читает — только оператор: GET /api/operator/audit?eventType=&actorType=&tenantId=&from=&to=&limit= (сортировка At DESC, limit ≤500). TTL/авто-очистка — не делаем (retention 180 дней и выгрузка — на усмотрение оператора, документируется в техдок §9); purge-скрипт — вне этапа.
  • Ruling 5 (д) — rate limiting и защита входа. Реализация — встроенный AddRateLimiter ASP.NET Core (политики-именованные, без нового NuGet) + прикладной guard. Порядок middleware: SessionMiddleware → OperatorSessionMiddleware → UseRateLimiter → OriginGuard → эндпоинты (политика «api» берёт ключ из CurrentUser.TenantId либо IP анонима — SessionMiddleware уже отработал). Политики и флаги — секция RateLimit (класс RateLimitOptions): Enabled (false в dev/тестах по умолчанию — curl-приёмки не режутся; true в PROD-окружении), AuthPerMinute (10/мин на IP для /api/auth/login и /api/operator/auth/login), ApiPerMinute (600/мин на тенанта/IP), GrpcIngressPerMinute (600/мин на тенанта gRPC-ингресса :5082, интерцептор IngressRateLimitInterceptor — фиксированное окно по metadata tenant-id; health освобождён). Ответ 429 — {"detail":"Слишком много запросов. Повторите позже"}. Лимит попыток входа — прикладной LoginAttemptGuard (singleton, in-memory фиксированное окно по ключу ip|normalizedLogin, как в прототипе лимитов нет — новый): ≥5 неудач за 15 мин → 429 «Слишком много попыток входа. Попробуйте через 15 минут»; успешный вход сбрасывает счётчик ключа. Один инстанс core (compose) — in-memory достаточно; multi-instance — задел (зафиксировать в техдок §11).
  • Ruling 6 (е) — mTLS за флагом. Dev остаётся как есть: plaintext + обязательный service-token (Ruling 2 этапа 6). Новое: секция/DEAL_MTLS_* (Enabled=false default, ServerCertPfx, ServerCertPassword, ClientCertPfx, ClientCertPassword, CaPem): при Enabled=true — (1) Kestrel внутренних gRPC-эндпоинтов (сервисы :5101–5103, ингресс core :5082) включает HTTPS с серверным сертификатом и требует клиентский сертификат (chain → CA из CaPem); (2) исходящие gRPC-клиенты core (Grpc*Client + health-пробы) и сервисы→ингресс подписывают запрос клиентским сертификатом и проверяют CA сервера. Основной HTTP :5080 core остаётся http — TLS терминирует Caddy (Ruling 9). Сертификаты генерируются скриптом scripts/mtls-certs.sh (openssl: dev-CA + серверные сертификаты на core, telegram-service, ai-service, ml-service, localhost + общий клиентский сертификат deal-client) в deploy/certs/ (в репозиторий не попадают — вне git, но и проект не git: каталог в .dockerignore/README-пометка). Код интерцепторов/контрактов не меняется — меняется только транспорт (решение-рамка этапа 6). Живое mTLS-рукопожатие — ⚠ Manual.
  • Ruling 7 (ж) — observability: минимально рабочий набор. Serilog добавляется во все четыре процесса (core + TG/AI/ML): консоль в формате JSON (prod-стиль; dev можно текст) + rolling-файл data/logs/deal-*.json (core — под volume). Секреты/пароли/ключи не логируются (правило уже есть). OTel-метрики/трейсы и Prometheus в этапе 7 не добавляем — объём ограничен, стек фиксируется как «Serilog-логи → Promtail → Loki → Grafana», метрики ASP.NET Core задекларированы в техдок §7 TODO (решение-рамка архитектуры §9 соблюдена наполовину: структурированные логи + дашборды по логам/health). Дашборды Grafana — минимальные (health-контейнеры и поиск по логам), provisioning- файлами (datasource Loki + dashboard JSON), без коммерческих плагинов.
  • Ruling 8 (з) — бэкапы. scripts/backup.sh: (1) Postgres — docker compose exec -T postgres pg_dump -Fc всех схем (public+tenant_*) → data/backups/pg/; (2) MinIO — mc mirror бакета deal-files в архив (или docker run-контейнер minio/mc); (3) файловые volume'ы telegram-сессий и ML-моделей (deal_tg_sessions, deal_ml_data) — docker run --rm -v-тар (busybox), сессии уже зашифрованы AES-GCM — архив без доп. шифрования, доступ только root; (4) core data (ключ шифрования DEAL_ENCRYPTION_KEY/файл + attachments, если Local) — тар. Retention: 14 копий (find -mtime +14 -delete), имя файла backup-YYYYMMDD-HHMMSS.*. Планировщик — вне контейнера: systemd timer/cron пример в шапке скрипта и техдок §9 (документировано, НЕ ставится скриптом). Восстановление — раздел в техдок §9 (шаги: поднять compose → pg_restore → распаковать volume → перезапуск сервисов). Реальный прогон бэкапа и restore-тест — ⚠ Manual (нужен docker).
  • Ruling 9 (и) — compose-prod и границы. PROD: сервисы postgres (без host-портов; volume), minio (без host-портов), core (:5080 в compose-сети + :5082 ингресс), telegram-service, ai-service, ml-service (mTLS env из Ruling 6), caddy (единственный наружу: 80/443; терминация TLS tls internal — для реального домена заменить на Cloudflare-origin/сертификаты, комментарий в Caddyfile; статика src/frontend/dist + reverse_proxy /api → core:5080; security-заголовки), loki/promtail (docker-логи по label'ам)/grafana (датасорс Loki, dashboard-провижининг, publish 127.0.0.1:3001:3000 — доступ оператору по SSH-туннелю). Секреты — только из .env (шаблон .env.prod.example, без дефолтных паролей — fail-fast на отсутствующие); healthcheck'и как в dev (grpc_health_probe/pg_isready); rate limiting включён, CORS — явный allowlist (Security:AllowedOrigins), куки Secure=true. Все сервисы — в одной внутренней сети, наружу — только caddy. Вне этапа: Cloudflare (конфигурация вне кода, документируется), k8s, биллинг-провайдер, саморегистрация, UI админки, multi-instance rate-limit. Живой подъём PROD — ⚠ Manual; авто-приёмка — docker compose -f deploy/compose.prod.yml config (rc=0).
  • Ruling 10 (к) — безопасность-доработки в коде. (1) Защита входа — Ruling 5. (2) Origin- проверка мутаций: OriginGuardMiddleware — для не-GET/HEAD/OPTIONS запросов /api, у которых есть заголовок Origin, значение обязано совпасть с Host запроса либо быть в allowlist Security:AllowedOrigins (CORS-дев-режим уже разрешает любой origin — middleware работает только с явным allowlist из конфига; при пустом списке правило = «Origin == Host»); несовпадение → 403. SameSite=Lax кук остаётся первым рубежом CSRF (документируется). (3) Security-заголовки: SecurityHeadersMiddleware на весь core (X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer); CSP/HSTS — на Caddy (фронт-статика; CSP для Vue требует аккуратной настройки nonce — документируется в техдок §10, фронт не меняется). (4) Секреты/параметризация SQL/Argon2id — уже есть, новых исключений не вводим. (5) Приостановка тенанта: вход заблокирован (AuthService проверяет статус тенанта через ITenantRepository.GetByIdAsync), ИИ-расход заморожен (гейт Ruling 3); активные тенант-сессии доживают до expiry (мгновенный разлогин — вне этапа, документируется). (6) IDOR: tenantId новых сущностей всегда из сессии/реестра, никогда из тела; перекрёстные проверки — unit-сценарии в задачах-владельцах + сквозной curl-сценарий финальной задачи (оператор против тенант-ручек и наоборот, чужой инвайт/чужой тенант).
  • Ruling 11 (л) — где живут новые ручки и кто их зовёт. Операторская админка — API-only под /api/operator/* (фронт не трогаем, UI админки — будущий отдельный инкремент): auth (login/logout/ me), тенанты (list/create/status/impersonate), инвайты (list/create/revoke), лимиты (view/change по тенанту + сводка usage), аудит (list), health (core/БД/сервисы). Публичная активация — /api/join. Ни одна из этих ручек не конфликтует с замороженным контрактом /api (api-map §3): тенантные /api/admin/* (tick/fts/check-message) остаются тенантными. Новых SSE-типов нет (используются существующие toast); событий pipeline_stats/boards_changed/leads_reclassified это не касается.

Задачи

Отчёты — task-N-report.md в .superpowers/sdd/deal-stage7-saas/. Пути сокращены по Rulings.

Task 1: SystemSaaS — public-таблицы оператора/инвайтов/лимитов/аудита + миграция

Files: Create: I/Persistence/Entities/{OperatorEntity,OperatorSessionEntity,InviteEntity, TenantLimitEntity,AuditLogEntity}.cs (поля по Rulings 1/3/4; PascalCase-свойства), I/Persistence/ {OperatorConfiguration,OperatorSessionConfiguration,InviteConfiguration,TenantLimitConfiguration, AuditLogConfiguration}.cs (ToTable("operators"|"operator_sessions"|"invites"|"tenant_limits"| "audit_log", "public"); unique: operators.Login, invites.Email partial (активные), FK: OperatorSessions →Operators (Cascade), Invites.CreatedById→Operators (Restrict), TenantLimits→Tenants (Restrict), AuditLog без FK; индексы AuditLog(At), AuditLog(TenantId, EventType)). Modify: I/Persistence/ DealDbContext.cs — DbSet'ы Operators/OperatorSessions/Invites/TenantLimits/AuditLog + ApplyConfiguration. EF: миграция SystemSaaS (dotnet ef migrations add SystemSaaS --context DealDbContext --output-dir Migrations --project src/core/Deal.Infrastructure --startup-project src/core/Deal.Api).

Источники: Rulings 1/3/4; эталоны TenantEntity/TenantConfiguration и SessionConfiguration; техдок §13.2 (команда system-миграции).

Acceptance: build 0/0; миграция применяется к dev-PG (нужен поднятый deal-postgres — если контейнер не поднят, применение и psql-проверка ⚠ Manual); psql: 5 новых таблиц в public, уникальные индексы на месте. Отчёт: task-1-report.md.

Task 2: Оператор — модели/порт/сервис auth, bootstrap из env, dev-only дефолтный тенант

Files: Create: TM/Application/Models/{StoredOperatorDto,OperatorIdentityDto,OperatorSessionDto, OperatorLoginResultDto}.cs; TM/Application/IOperatorAuthStore.cs (FindByLogin/Create/FindSession/ CreateSession/DeleteSession/DeleteExpired), TM/Application/OperatorAuthService.cs (Login/Logout/ ResolveSession; срок жизни 12 ч, нормализация login, Argon2id через IPasswordHasher — эталон AuthService), TM/Application/OperatorBootstrapService.cs (IHostedService-подобный шаг внутри существующего TenantBootstrapService или отдельным hosted после него — идемпотентно: env DEAL_OPERATOR_LOGIN/PASSWORD, в Development дефолт operator/operator, в Production без env — warning и пропуск). Modify: A/Hosting/TenantBootstrapService.cs — дефолтный тенант создаётся только в Development/DEAL_BOOTSTRAP_DEFAULT_TENANT=1 (Ruling 1); A/Configuration/CookieOptions.cs или новый OperatorCookieOptions — секция OperatorCookies (Name=deal_operator_session, Secure из конфига). Tests: T/OperatorAuthServiceTests.cs (login ok/неверный пароль/нормализация/12 ч expiry), T/FakeOperatorAuthStore.cs; bootstrap (идемпотентность, dev-default, prod-без env → skip).

Источники: AuthService/SessionTokens/TenantBootstrapService (эталоны); Rulings 1.

Acceptance: build 0/0; unit PASS. Отчёт: task-2-report.md.

Task 3: Оператор — HTTP-контур /api/operator/auth + операторская сессия

Files: Create: A/Middleware/OperatorSessionMiddleware.cs (кука deal_operator_session → OperatorAuthService.ResolveSession → HttpContext.Items["CurrentOperator"]; pass-through как SessionMiddleware; Reset не нужен — общий ITenantContext не трогается), A/Http/AuthHelpers.cs — добавить GetCurrentOperator()/RequireOperator (403 «Требуется вход оператора» или 401 — согласовать с текстами: для /api/operator/* без операторской сессии — 401 {"detail": "Требуется вход оператора"}), A/Endpoints/OperatorAuthEndpoints.cs (POST login/logout, GET me — тела/ответы как AuthEndpoints, текст ошибки «Неверный логин или пароль оператора»). Modify: A/Program.cs — регистрация OperatorAuthService/IOperatorAuthStore (AddTenantsModule расширяется), UseMiddleware<OperatorSessionMiddleware>(), MapOperatorAuthEndpoints(), секция OperatorCookies. Login-попытки пишут аудит-события (Task 4) — на этом шаге заглушка-вызов отсутствует, добавится в Task 4.

Источники: AuthEndpoints/SessionMiddleware/CookieOptions (эталоны); api-map §3.1 (форма ответов); Rulings 1/4.

Acceptance: build 0/0; curl-приёмка на :5080 (Postgres поднят): login operator/operator → кука deal_operator_session + {ok:true,login}; GET /api/operator/auth/me → login; неверный пароль → 401; logout → ok и 401 после; тенантная кука deal_session НЕ проходит на /api/operator/auth/me (401); операторская кука НЕ проходит на /api/auth/me (401). Отчёт: task-3-report.md.

Task 4: Аудит-поток — AuditService, события входов, чтение оператором

Files: Create: TM/Application/AuditEvents.cs (константы Ruling 4), TM/Application/Models/ AuditRecordDto.cs, TM/Application/IAuditLogStore.cs (AppendAsync/QueryAsync(filter)/— без Update/ Delete), TM/Application/AuditService.cs (Append через store; хелперы ActorFromUser/Operator), I/Persistence/Repositories/AuditLogStore.cs (EF: Append — Add+Save; Query — фильтры At-range/ EventType/TenantId/ActorType, At DESC, limit ≤500), регистрация в I/ServiceCollectionExtensions.cs (AddDealPersistence). Modify: A/Endpoints/AuthEndpoints.cs и A/Endpoints/OperatorAuthEndpoints.cs — после успеха/неудачи login вызывают AuditService.Append (tenant_login_ok/failed с login и IP, operator_login_*); tenant_login_failed пишется и при неверном пароле, и при заблокированном (suspended) входе (Task 7). Create: A/Endpoints/OperatorAuditEndpoints.cs (GET /api/operator/audit с фильтрами-query; ответ {items:[…], total}). Tests: T/AuditServiceTests.cs, T/FakeAuditLogStore.cs; endpoint-хелперы фильтров.

Источники: Rulings 4; эталон DiscoveryLogService/DiscLog (паттерн лога); техдок §10 (аудит-лог).

Acceptance: build 0/0; unit PASS (append-only: у порта нет Update/Delete); curl: failed login → запись audit (psql или GET /api/operator/audit), успешный login → запись ok. Отчёт: task-4-report.md.

Task 5: Инвайты — сервис/адаптер/операторские ручки + аудит

Files: Create: TM/Application/Models/InviteDto.cs, TM/Application/IInviteStore.cs (Create/GetByCode/List/UpdateStatus/FindActiveByEmail), TM/Application/InviteCodeGenerator.cs (url-safe, 16 симв.), TM/Application/InvitesService.cs (CreateInvite(tenantId?, email) — валидация email, одна активная на email → 400 «Для этого email уже есть активное приглашение», expiry = +72 ч; Revoke; List; GetByCode с вычислением статуса expired при чтении), I/Persistence/Repositories/ InviteStore.cs. Modify: A/Endpoints/ — создать A/Endpoints/OperatorInvitesEndpoints.cs (GET list, POST create {email, tenantId?}, POST {code}/revoke; ответы: create → {code, email, tenantId?, expiresAt, status}; revoke → {ok:true}), вызовы AuditService (invite_created/invite_revoked с email и code в DetailJson). Tests: T/InvitesServiceTests.cs, T/FakeInviteStore.cs (создание/expiry при чтении протухшего/revoke/дубль email на активном/revoked позволяет новый); curl-минимум на ручки (create → list → revoke, 401 без оператора).

Источники: Rulings 2/4; эталон DiscoveryTasksService (валидации/статусы); ТЗ §3 (инвайты).

Acceptance: build 0/0; unit PASS; curl-сценарий ручек PASS. Отчёт: task-5-report.md.

Task 6: Активация инвайта — POST /api/join (пользователь + тенант + провижининг)

Files: Create: A/Endpoints/JoinEndpoint.cs (POST /api/join {code,email,name?,password}; без сессии): InvitesService.GetByCode (expired → 400 «Срок действия приглашения истёк»; статус ≠ pending → 400 «Приглашение уже использовано»/«отозвано»), сверка email (400 «Email не совпадает с приглашением»), существующий users.login (400 «Этот email уже зарегистрирован»), создание тенанта при TenantId=null через TenantService.CreateTenantAsync(name ?? email, new Guid) + создание пользователя authStore.CreateUserAsync (Argon2id, login=email), TenantLimits-строка с дефолт-бюджетом (Ruling 3 — вставка через порт ITenantLimitStore из Task 8; до Task 8 допускается прямая вставка адаптером Task 1-таблицы в этой же задаче — см. Task 8), статус invite → activated, аудит invite_activated. Ответ: {ok:true, login} (кука НЕ ставится — далее обычный /api/auth/login). Валидация пароля ≥4 (текст как в AuthEndpoints). Modify: регистрация MapJoinEndpoint() в Program.cs. Tests: T/JoinFlowTests.cs — модульный сценарий на Fake-сторах: код+email+пароль → пользователь + тенант (создан через фейк-провижинер, вызван 1 раз) + invite activated + аудит; ошибки (код/email/ дубль/протух/revoked).

Источники: Rulings 2/3/11; TenantService.CreateTenantAsync + AuthService (эталоны создания); ТЗ §3 (инвайты/регистрация).

Acceptance: build 0/0; unit PASS; curl-сценарий: оператор создаёт инвайт → /api/join (новый email) → psql: тенант в tenants + схема tenant_* провижинена + пользователь в users + invite activated; повторный /api/join тем же кодом → 400. Отчёт: task-6-report.md.

Task 7: Оператор-тенанты — список/создание/статус/приостановка/impersonation

Files: Create: A/Endpoints/OperatorTenantsEndpoints.cs: GET /api/operator/tenants (реестр + счётчики: пользователи, статус, бюджет/использовано — чтение лимитов из Task 8 по мере готовности; на этом шаге — без лимит-полей или через Task 8-порт после него), POST /api/operator/tenants {name, email?, budget?} — email-опция создаёт сразу пользователя-владельца тенанта (иначе — через инвайт), PATCH /api/operator/tenants/{id} {status: "active"|"suspended"} (аудит tenant_status_changed), POST /api/operator/tenants/{id}/impersonate {login?} — mint сессии целевого пользователя (переиспользуя механизм AuthService.CreateSession), ответ {sessionToken, expiresAt, tenantId} + аудит impersonation_started (DetailJson: targetLogin, tenantId); завершение — logout'ом пользователя (документируется). Modify: TM/Application/ITenantRepository.cs + I/Persistence/Repositories/TenantRepository.csGetByIdAsync/UpdateStatusAsync; TM/Application/AuthService.cs — Login блокирует suspended-тенант (LoginResultDto получает опциональный Error = "tenant_suspended", endpoint-текст «Учётная запись приостановлена. Обратитесь к оператору»). Tests: T/TenantAdminServiceTests-сценарии или прямо на сервисах (suspend → login заблокирован; impersonation: оператор ≠ тенант — сессия выдаётся пользователю тенанта, а не оператору; аудит-записи); IDOR-кейсы: оператор не читает settings тенанта, тенант не вызывает /operator (403/401 — через curl финальной задачи).

Источники: Rulings 1/4/10; TenantService/AuthService/SessionTokens; ТЗ §10 (тенанты/impersonation).

Acceptance: build 0/0; unit PASS; curl-минимум: create → suspend → login тенанта 401-текст → resume → login ok; impersonate → полученный токен работает как deal_session на /api/auth/me. Отчёт: task-7-report.md.

Task 8: Лимиты-ядро — хранилище/период/рекордер/дефолт-бюджет

Files: Create: TM/Application/Models/{TenantLimitDto,BudgetStateDto}.cs (BudgetState: TenantId, BudgetTokens, Period, PeriodStart, UsedTokens, Status, Allowed, Warned80, NotifiedExhausted), TM/Application/ITenantLimitStore.cs (GetOrCreateAsync(tenantId, defaults), GetStateAsync, AddUsageAsync (инкремент + ленивый reset периода + пересчёт флагов в одной транзакции/сохранении), UpdateBudgetAsync (сброс флагов), TryMarkWarned/Notified), TM/Application/TokenBudgetDefaults.cs (DefaultBudgetTokens = 10_000_000, Period = month), TM/Application/TokenBudgetService.cs (период-математика: начало периода, ленивый reset, пороги 80/100), I/Persistence/Repositories/TenantLimitStore.cs (EF на DealDbContext; AddUsage — UPDATE tenant_limits SET UsedTokens = UsedTokens + @n ... через ExecuteSql не используем — читаем строку и пишем в транзакции с rowversion-семантикой: одиночный инстанс core, конкурентность на тенанта сериализована воркер-гейтами; фиксируем простое read-modify-write). Modify: I/Integrations/AiUsageLedger.cs → переименовать/расширить до TokenUsageRecorder (добавляет вызов ITenantLimitStore.AddUsageAsync поверх lifetime-KV aiTokenUsage); call-site'ы в I/Integrations/GrpcAiClassifier.cs и I/Integrations/GrpcAiTools.cs. Тесты: T/TokenBudgetServiceTests.cs (reset месяца/дня, пороги, дефолты), T/FakeTenantLimitStore.cs.

Источники: Rulings 3/4; AiUsageLedger/GrpcAiClassifier (эталон учёта); ТЗ §9; архитектура §19.

Acceptance: build 0/0; unit PASS (ленивый reset: запись с PeriodStart прошлого месяца обнуляет UsedTokens и ставит новый PeriodStart; порог 80% выставляет Warned80). Отчёт: task-8-report.md.

Task 9: Бюджетный гейт ИИ + fallback-декораторы + SSE-уведомления

Files: Create: I/Integrations/BudgetedAiClassifier.cs, I/Integrations/BudgetedAiTools.cs (декораторы портов IAiClassifier/IAiTools: перед каждым вызовом ITokenBudgetGate (или ITenantLimitStore.GetStateAsync + TokenBudgetService) — исчерпано/suspended → Local-реализации (классификатор/фильтр) или AiUnavailableException (инструменты); gRPC-адаптеры не меняются), A/Hosting/BudgetAlertScheduler.cs (60 с, per-tenant: GetState → переход 80/100% → SseBroker-тост + TryMarkWarned/Notified; сброс флагов при смене бюджета уже в Task 8). Modify: I/Integrations/ ServiceCollectionExtensions.cs/AddDealIntegrations — регистрация декораторов только при Services:Ai:UseLocal=false (порядок: Grpc → Budgeted → наружу), регистрация TokenUsageRecorder, TokenBudgetService, ITenantLimitStore (scoped), BudgetAlertScheduler в A/Program.cs. Тесты: T/BudgetedAiClassifierTests.cs (лимит 0 → Local-ветка; лимит большой → gRPC-фейк вызван; suspended → Local), T/BudgetedAiToolsTests.cs (исчерпано → AiUnavailableException), тест BudgetAlertScheduler-логики на фейках (тост один раз на порог).

Источники: Rulings 3/5/7/11; LocalAiClassifier/LocalAiTools/AiUnavailableException (эталон fallback); StorageTickScheduler/SseBroker (эталон тостов); ТЗ §9.

Acceptance: build 0/0; unit PASS. Отчёт: task-9-report.md.

Task 10: Оператор-лимиты/usage/health — эндпоинты

Files: Create: A/Endpoints/OperatorLimitsEndpoints.cs (GET /api/operator/limits — сводка по всем тенантам {items:[{tenantId, name, budget, period, used, percent, status}]}; GET/PATCH /api/operator/tenants/{id}/limit — просмотр/смена {budget?, period?}; PATCH сбрасывает Warned80/NotifiedExhausted; аудит tenant_limit_changed), A/Endpoints/OperatorHealthEndpoints.cs (GET /api/operator/health: core+БД (SELECT 1 через DealDbContext) + gRPC-health ml/ai/telegram по Services:*:Endpoint через Grpc.HealthCheck-клиента; при UseLocal=true — {reachable:false, mode:"local"}), I/Integrations/ServiceHealthProbe.cs (gRPC health-проба с таймаутом 3 с, клиентские сертификаты из Ruling 6-конфига). Tests: T/ServiceHealthProbeTests.cs (in-proc health-сервер фейк), хелперы percent-расчёта.

Источники: Rulings 3/9/11; DiscoveryEndpoints (формат items), техдок §8 (health); ТЗ §10 (health, лимиты).

Acceptance: build 0/0; unit PASS; curl: GET/PATCH лимита оператором (psql-проверка строки), health-эндпоинт 200 (в dev Local-режиме сервисы помечены local). Отчёт: task-10-report.md.

Task 11: Rate limiting (приложение + gRPC-ингресс) и защита входа

Files: Create: A/Configuration/RateLimitOptions.cs (Enabled, AuthPerMinute=10, ApiPerMinute=600, GrpcIngressPerMinute=600, LoginAttemptsMax=5, LoginAttemptWindowMin=15), A/Middleware/ RateLimitPolicies.cs (AddRateLimiter: политики auth — fixed window по IP, api — по CurrentUser.TenantId/IP анонима; OnRejected → 429 {detail:"Слишком много запросов. Повторите позже"}), A/Http/LoginAttemptGuard.cs (in-memory окно ip|login, блок 15 мин после 5 неудач, сброс при успехе), A/Telegram/IngressRateLimitInterceptor.cs (gRPC: фиксированное окно по metadata tenant-id, health-метод освобождён). Modify: A/Program.csAddRateLimiter (если Enabled), порядок middleware (Session → Operator → RateLimiter), RequireRateLimiting на группах auth/operator/auth; A/Endpoints/AuthEndpoints.cs/OperatorAuthEndpoints.cs — вызов LoginAttemptGuard до AuthService; A/Program.cs Kestrel-gRPC — AddGrpc interceptor при Enabled. Tests: T/LoginAttemptGuardTests.cs (5 неудач → блок, успех сбрасывает), unit политики-ключей (tenant vs IP), interceptor-окно.

Источники: Rulings 5/10; IngressServiceTokenInterceptor (эталон); техдок §8/§10 (rate limit).

Acceptance: build 0/0; unit PASS; dev-прогон не режет curl-приёмки (Enabled=false). Отчёт: task-11-report.md.

Task 12: Безопасность — Origin-проверка, security-заголовки, CORS-allowlist

Files: Create: A/Configuration/SecurityOptions.cs (AllowedOrigins string[]), A/Middleware/ OriginGuardMiddleware.cs (не-GET/HEAD/OPTIONS и есть Origin → Origin ∈ {Host} AllowedOrigins, иначе 403), A/Middleware/SecurityHeadersMiddleware.cs (X-Content-Type-Options/X-Frame-Options/ Referrer-Policy). Modify: A/Program.cs — порядок middleware и регистрация (после RateLimiter), CORS-политика: при пустом AllowedOrigins — dev-режим «любой» (текущий), при непустом — строгий allowlist+credentials (для PROD). Tests: T/OriginGuardTests.cs (совпадение Host ok, чужой Origin → 403, allowlist ok, GET без Origin ok), headers-присутствие (in-proc host или unit на делегате).

Источники: Rulings 10; архитектура §8 (CSRF/XSS/headers); техдок §10 (прокси-заголовки — теперь и кодом).

Acceptance: build 0/0; unit PASS; curl: мутация с Origin: http://evil → 403, без Origin → ok. Отчёт: task-12-report.md.

Task 13: mTLS — флаг/сертификаты в 4 процессах + скрипт генерации

Files: Create: scripts/mtls-certs.sh (openssl: CA + серверные PFX для core/telegram/ai/ml + клиентский сертификат deal-client; SAN: localhost + имена compose-сервисов; вывод в deploy/certs/), в каждом процессе класс MtlsOptions (Enabled/ServerCertPfx/ServerCertPassword/ ClientCertPfx/ClientCertPassword/CaPem; env DEAL_MTLS_*) и его применение: Modify: TG/Deal.Telegram/ Program.cs, AI/Deal.Ai/Program.cs, ML/Deal.Ml/Program.cs (Kestrel gRPC-endpoint: UseHttps(serverPfx, opts => opts.ClientCertificateMode = RequireCertificate; opts.ClientCertificateValidation = цепочка на CaPem); исходящий канал в core-ингресс — клиентский сертификат), A/Program.cs (ингресс :5082 — аналогично) и I/Integrations/*GrpcConnection.cs (каналы: HttpClientHandler с клиентским сертификатом + проверка CA, только при Enabled). Dev-дефолт неизменен (plaintext). Тесты: unit на опции/загрузку сертификата из файла (тестовые PFX генерируются в тесте скриптом? нет — фиктивные сертификаты через CertificateRequest в памяти). Живое mTLS-рукопожатие между контейнерами — ⚠ Manual.

Источники: Rulings 6; этап 6 Ruling 2 (рамка dev/prod); техдок §8/§10; архитектура §8.

Acceptance: build 0/0 (все sln); sh -n scripts/mtls-certs.sh; unit PASS; PROD-compose-файл ссылается на env mTLS (Task 14). Отчёт: task-13-report.md.

Task 14: Observability + compose.prod (Caddy/Loki/Promtail/Grafana)

Files: Create/Modify: Serilog — A/Program.cs, TG|AI|ML/.../Program.cs (Serilog JSON console + rolling file data/logs/; конфиг из appsettings/env; секреты не логируются), csproj'ы + пакеты. Create: PROD (postgres/minio/core/3 сервиса по compose.dev.yml-образцу, но: без host-портов у хранилищ, mTLS-env из Ruling 6, rate-limit/CORS/куки-Secure-флаги, depends_on-healthcheck'и, frontend-сборка — из src/frontend/dist volume, комментарий), deploy/caddy/Caddyfile (80/443, tls internal, статика dist, reverse_proxy /api/* core:5080, security-заголовки, CSP-комментарий), deploy/observability/promtail.yml (docker_sd, labels, loki-адрес), deploy/observability/loki.yml (local-storage, retention 7d), deploy/observability/grafana/ {datasources.yml, dashboards/Deal-Health.json} (Loki-датасорс, минимальный health/лог-дашборд), deploy/.env.prod.example (все секреты БЕЗ значений-дефолтов). Modify: техдок §7/§8 (актуализация под реальные файлы) — в Task 16 (доки). Acceptance-без-docker: docker compose -f PROD config rc=0 (если docker CLI недоступен — ⚠ Manual). Живой подъём PROD-стека — ⚠ Manual.

Источники: Rulings 6/7/9; compose.dev.yml (эталон); техдок §7/§8/§10; архитектура §9/§10.

Acceptance: build 0/0 всех sln; старт Api (dev, без docker) показывает JSON-логи в консоли/файле; PROD config валиден. Отчёт: task-14-report.md.

Task 15: Бэкапы — scripts/backup.sh + документация восстановления

Files: Create: scripts/backup.sh (Ruling 8: pg_dump -Fc через compose exec; mc mirror MinIO или minio/mc-контейнер; tar volume'ов сессий/ML/core-data через busybox-контейнер; retention 14; имена backup-<ts>.*; trap-очистка; заголовок с примером systemd-timer/cron; exit non-zero при сбое любого шага), docs/technical/... §9 — раздел «Восстановление» (шаги pg_restore/распаковка volume/перезапуск; тест восстановления раз в месяц) — в Task 16. Acceptance: sh -n scripts/backup.sh; прогон скрипта и restore-тест — ⚠ Manual (нужен docker-стек PROD/DEV).

Источники: Rulings 8; техдок §9 (текущий текст — основа); архитектура §18 (ежедневные бэкапы).

Acceptance: sh -n rc=0; скрипт покрывает 4 источника данных из Ruling 8; retention-логика читаема. Отчёт: task-15-report.md.

Task 16: Финал — доки, сквозная SaaS-приёмка, полный прогон

  • Доки: техдок — §11 (TODO-сводка: закрыть пункты этапа 7, оставить только реальные заделы: OTel-метрики, multi-instance rate-limit, мгновенный разлогин suspended, ML-экспорт, reclassify на реальном ИИ, мультиаккаунтность, k8s/биллинг/саморегистрация/UI-админки), новый блок §13.8 (этап 7: оператор/инвайты/лимиты/аудит/rate-limit/mTLS/бэкапы/compose-prod — быстрый старт оператора), §7/§8/§9/§10 актуализируются по ходу (compose.prod, бэкапы-restore, Serilog/Loki, mTLS-флаги, Origin/заголовки, ограничения in-memory guard); api-map — раздел «Этап 7 (API-only, фронт не вызывает)»: /api/operator/* + /api/join (формы/ответы); roadmap — этап 7 «Выполнено» (ограничения → заделы), «Открытые точки» — закрыть п.2 (инвайты/оператор реализованы, dev-seed остаётся dev-only); STATUS.md — строка этапа 7 , проценты, «Итого»; docs/user-guide/Инструкция-пользователя-Дейл.md — раздел «Регистрация по приглашению» (как оператор пришлёт, как активировать, что такое бюджет ИИ и fallback-уведомление).
  • Сквозная SaaS-curl-приёмка (dev-stack, Postgres; без docker-сервисов — AI в Local-режиме, бюджет-сценарий проверяется через Local-счётчики/прямые вызовы; полный стек с сервисами — ⚠ Manual после поднятия Docker, sh scripts/dev-smoke.sh + бюджет-прогон): оператор login → создать тенанта → инвайт → /api/join → вход тенанта → работа /api (me/settings) → оператор: лимит-бюджет мал → симуляция ИИ-вызова (через recorder) → fallback-декоратор (Local-ветка) → тост-флаг в tenant_limits → аудит-лента (входы/инвайты/impersonation) → suspend → login 401 → resume → IDOR-негативы (тенант на /operator → 401, чужой tenantId в /operator-фильтрах не отдаёт чужие данные, чужой инвайт-код/email → 400).
  • Полный прогон: scripts/build.sh + scripts/test.sh (830 + новые unit PASS), build каждого сервисного sln 0/0; итоговые числа в отчёт.
  • Отчёт task-16-report.md + финальная строка progress.md.

Источники: Rulings 1–11; все предыдущие задачи; паттерны финальных задач этапов 1–6.

Acceptance: пункты выше; живые проверки (docker-стек, mTLS, бэкап, реальные LLM/Telegram) — ⚠ Manual и помечены в отчёте.

Self-Review

  1. Spec coverage: оператор/роли/изоляция — Rulings 1, Task 2/3/7; инвайты + invite-only + email-unique — Rulings 2, Task 5/6 (ТЗ §3); лимиты-бюджеты/fallback/уведомление — Rulings 3, Task 8/9 (ТЗ §9); админка (тенанты/статусы/лимиты/health/impersonation/аудит/подозрительная активность) — Rulings 4/11, Task 4/5/7/10 (ТЗ §10; «подозрительная активность» = операторский фильтр по audit eventType login_failed, документируется); rate limiting + попытки входа — Ruling 5, Task 11; mTLS/service-token — Ruling 6, Task 13; observability (Serilog+Loki+Grafana, минимально) — Ruling 7, Task 14; бэкапы — Ruling 8, Task 15; compose-prod — Ruling 9, Task 14; безопасность-доработки (Origin/headers/IDOR/приостановка) — Ruling 10, Task 7/12 (+IDOR-кейсы в Task 57, сквозные в Task 16); финальные доки/приёмка — Task 16. Решения владельца учтены: dev-seed admin/admin остаётся dev-only (Ruling 1), «ELF — B» = Loki+Promtail+Grafana (Ruling 7), «Админка А» = API-контур оператора без фронта (Rulings 1/11), бэкапы раз в сутки (Ruling 8), лимиты в токенах с fallback (Ruling 3), «compose, k8s отложен» (Ruling 9).
  2. Placeholder scan: TODO/«позже сделать» не закладывается внутрь задач; осознанно вынесено за этап (см. п.4). AiUsageLedger не «висит» дублирующим механизмом — он становится TokenUsageRecorder с той же точкой вызова (Ruling 3). Fallback-семантика переиспользует существующие Local-реализации, новых «заглушек» не появляется. OpenAPI-карта операторских ручек фиксируется в api-map (Task 16), отдельной спеки не создаём.
  3. Type consistency: все новые порты — в модуле Tenants (IOperatorAuthStore/IInviteStore/ ITenantLimitStore/IAuditLogStore), адаптеры — I/Persistence/Repositories/* (регистрация в AddDealPersistence), сервисы — TM/Application/* (реестр AddTenantsModule расширяется в Task 3); декораторы бюджета реализуют существующие порты IAiClassifier/IAiTools и регистрируются последними в AddDealIntegrations (внешний контракт для PL/Discovery не меняется); изменения AuthService/LoginResultDto — обратносовместимы (опциональное поле); TenantBootstrapService меняет только условие создания дефолтного тенанта (dev/prod), провижининг — всегда. Циклов ссылок нет: TM не знает Api/Infrastructure, Infrastructure оркестрирует, Api вызывает сервисы модуля и шлёт SSE.
  4. Вне scope этапа 7 (заделы): UI операторской админки и UI активации (API-only + curl); OTel-метрики/Prometheus и дашборды метрик (задекларировано, Ruling 7); multi-instance rate-limit и бэкенд для попыток входа (in-memory, один инстанс); мгновенный разлогин suspended-сессий; экспорт/импорт ML-моделей; reclassify на реальном ИИ; мультиаккаунтность Telegram на тенанта; биллинг-провайдер/планы; k8s/Cloudflare-конфигурация; purge/retention-автоматика audit_log; auto-purge tenant_limits-истории. Все перечислены в техдок §11 (Task 16).

Manual-пункты этапа (требуют docker/живых кред): применение system-миграции и curl-приёмки без поднятого deal-postgres невозможны (Postgres — контейнер dev-stack, поднимается по требованию); живой подъём compose.prod.yml и dev-smoke.sh-прогон полного стека с сервисами (Task 14/16); mTLS-рукопожатие между контейнерами (Task 13); реальный прогон scripts/backup.sh и restore-тест (Task 15); реальные LLM/Telegram-проверки — с кредами (вне этапа, как и в этапе 6).