Table of Contents
- Дейл (Deal) — Этап 7: SaaS-контур (оператор, инвайты, лимиты, аудит, безопасность, prod-деплой, финальные доки) Implementation Plan
- Global Constraints
- Зафиксированные решения (Rulings этапа)
- Задачи
- Task 1: SystemSaaS — public-таблицы оператора/инвайтов/лимитов/аудита + миграция
- Task 2: Оператор — модели/порт/сервис auth, bootstrap из env, dev-only дефолтный тенант
- Task 3: Оператор — HTTP-контур /api/operator/auth + операторская сессия
- Task 4: Аудит-поток — AuditService, события входов, чтение оператором
- Task 5: Инвайты — сервис/адаптер/операторские ручки + аудит
- Task 6: Активация инвайта — POST /api/join (пользователь + тенант + провижининг)
- Task 7: Оператор-тенанты — список/создание/статус/приостановка/impersonation
- Task 8: Лимиты-ядро — хранилище/период/рекордер/дефолт-бюджет
- Task 9: Бюджетный гейт ИИ + fallback-декораторы + SSE-уведомления
- Task 10: Оператор-лимиты/usage/health — эндпоинты
- Task 11: Rate limiting (приложение + gRPC-ингресс) и защита входа
- Task 12: Безопасность — Origin-проверка, security-заголовки, CORS-allowlist
- Task 13: mTLS — флаг/сертификаты в 4 процессах + скрипт генерации
- Task 14: Observability + compose.prod (Caddy/Loki/Promtail/Grafana)
- Task 15: Бэкапы — scripts/backup.sh + документация восстановления
- Task 16: Финал — доки, сквозная SaaS-приёмка, полный прогон
- Self-Review
Перенесено из репозитория (
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 (безопасность, L187–216),
§9 (observability/админка/бэкапы, L216–233), §10 (деплой, L233–243), §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 L82–84, 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-Postgresdeal-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 оператора: envDEAL_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, defaultmonth), 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-сумму в KVaiTokenUsage(существующий ключ — счётчик «всего», оператор/будущий 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 и защита входа. Реализация — встроенный
AddRateLimiterASP.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— фиксированное окно по metadatatenant-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=falsedefault,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) coredata(ключ шифрования 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; терминация TLStls 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 запроса либо быть в allowlistSecurity: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.cs — GetByIdAsync/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.cs — AddRateLimiter (если
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
- 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 5–7, сквозные в 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).
- Placeholder scan: TODO/«позже сделать» не закладывается внутрь задач; осознанно вынесено за этап (см. п.4). AiUsageLedger не «висит» дублирующим механизмом — он становится TokenUsageRecorder с той же точкой вызова (Ruling 3). Fallback-семантика переиспользует существующие Local-реализации, новых «заглушек» не появляется. OpenAPI-карта операторских ручек фиксируется в api-map (Task 16), отдельной спеки не создаём.
- 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. - Вне 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).