АРХ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: актуализация roadmap NDC platform

This commit is contained in:
DCCONSTRUCTIONS 2026-05-09 12:33:54 +03:00
parent 78deab1a23
commit ca9fd34e91
1 changed files with 579 additions and 0 deletions

View File

@ -20,6 +20,7 @@ WORKSPACE_SLUG = "nodedc"
PROJECT_IDENTIFIER = "NDCPLATFORM"
PROJECT_NAME = "NDC platform"
CODEX_EMAIL = "codex@nodedc.local"
PLATFORM_OWNER_EMAIL = "dcctouch@gmail.com"
STATE_TEMPLATES = [
{"group": "backlog", "name": "В обсуждении", "color": "#60646C", "default": True},
@ -595,6 +596,29 @@ server/control-plane-store.mjs переведен на atomic write: запис
Ограничение: email/password/profile update уже работает как dev-flow, но production UX для reset-password/invite-email и подтверждения смены email еще нужно вынести в отдельный полноценный этап.
""",
),
text_block(
"launcher",
"Актуализация 2026-05-09",
"""
За последние итерации Launcher фактически стал текущим control-plane для корпоративного контура NODE.DC. Через него уже проверяются: клиенты, участники, группы, инвайты, сервисный каталог, матрица доступов, Operational Core workspace binding, user profile, client avatar, fullscreen admin panels и service-specific role modal для Operational Core.
Зафиксирован продуктовый принцип: enterprise-контур управляется через Launcher. Для таких пользователей и workspace Launcher является source of truth по приглашениям, сервисному доступу и базовым ролям; downstream-приложения получают техническую проекцию и не должны становиться вторым независимым источником прав.
Отдельно вынесена новая большая карточка "Публичный контур пользователей". Она закрывает сценарий обычных внешних пользователей, которые приходят не через клиентскую компанию, а через запрос приглашения и дальше используют отдельные сервисы как standalone-продукты.
""",
),
checker(
"launcher-actual-20260509",
"Чекер актуализации 2026-05-09",
[
{"text": "Зафиксировать Launcher как source of truth для enterprise-доступов.", "checked": True},
{"text": "Зафиксировать Operational Core role modal как текущую модель детальных назначений.", "checked": True},
{"text": "Отделить public/self-service контур в самостоятельную карточку.", "checked": True},
"Заменить JSON-backed store на production persistence.",
"Описать recovery/MFA/email-change UX без раскрытия Authentik UI.",
"Вынести billing/limits в отдельный RFC после стабилизации public-контура.",
],
),
],
},
{
@ -867,6 +891,560 @@ Plane должен оставаться самостоятельным прод
"Запретить прямые записи Launcher в Plane DB.",
],
),
text_block(
"plane",
"Актуализация 2026-05-09",
"""
Текущая модель Operational Core разделена на два режима. Для workspace, которыми управляет Launcher, участники/инвайты/права должны быть скрыты или переведены в readonly внутри Task Manager, чтобы не получить конфликтующий source of truth. Для standalone/public workspace штатные механики Task Manager остаются включенными: пользователь может создавать workspace, приглашать участников и управлять проектами внутри продукта.
Практически проверено: корпоративные назначения из Launcher доходят до Operational Core, public-пользователь может создать workspace после выдачи доступа, а прямой доступ к сервису проходит через NODE.DC SSO. Safari-only падение workspace зафиксировано как отдельный deferred debug, потому что Chrome/Chromium flow работает и проблема не должна блокировать платформенную обвязку.
Открытая развилка: нужно формально добавить managedBy=launcher/managedBy=tasker или эквивалентный флаг в mapping workspace, чтобы интерфейс Task Manager понимал, когда скрывать собственное управление пользователями, а когда оставлять автономный SaaS-режим.
""",
),
checker(
"plane-actual-20260509",
"Чекер актуализации 2026-05-09",
[
{"text": "Сохранить автономность Task Manager как standalone-продукта.", "checked": True},
{"text": "Зафиксировать managedBy=launcher для enterprise workspace.", "checked": True},
{"text": "Зафиксировать managedBy=tasker для standalone/public workspace.", "checked": True},
"Скрыть или readonly-заблокировать Task Manager users/invites для managedBy=launcher.",
"Оставить Task Manager users/invites включенными для managedBy=tasker.",
"Очистить оставшиеся demo users/seed data без удаления живых связей.",
"Оформить Safari-only workspace crash как отдельный deferred debug.",
],
),
],
},
{
"slug": "design-guide-canon",
"name": "NDC дизайн-гайд: единый UI-код платформы",
"priority": "medium",
"state_group": "cancelled",
"assignees": [CODEX_EMAIL],
"description_html": html(
"Карточка сохранена как отложенная архитектурная ссылка: реальный дизайн-канон NODE.DC живет в HDESIGN-CODE.md, а не в теле канбан-карточки.",
"Цель актуализации: убрать конфликт источников истины и зафиксировать, что дальнейшие UI-работы должны сверяться с документом дизайн-канона и существующими launcher/tasker компонентами.",
"Статус: deferred. Карточка не является активной задачей разработки до решения выделять дизайн-систему в отдельный пакет.",
),
"blocks": [
text_block(
"design-guide",
"Текущая архитектура",
"""
Единый визуальный канон NODE.DC зафиксирован в репозитории Operational Core в документе HDESIGN-CODE.md. Он описывает glass/card shell, top-bar, admin overlay, auth screens, spacing, typography, motion, token usage и правила интеграции в Launcher/Task Manager.
Карточка NDCPLATFORM-7 больше не должна конкурировать с этим документом и не должна хранить длинный HTML-дизайн-гайд. Канбан-карточка остается навигационной отметкой: дизайн-канон существует, но вынос в отдельную design-system работу пока отложен.
""",
),
text_block(
"design-guide",
"Этап 1. Зафиксировать источник дизайн-канона",
"""
Статус: выполнено.
HDESIGN-CODE.md является source of truth для текущего UI-кода NODE.DC. При разработке Launcher/admin overlay, auth screens и Task Manager NODE.DC-слоев сверяемся с ним, а не с историческим HTML-телом карточки.
""",
),
checker(
"design-guide1",
"Чекер этапа 1. Источник дизайн-канона",
[
{"text": "Признать HDESIGN-CODE.md реальным дизайн-каноном.", "checked": True},
{"text": "Оставить NDCPLATFORM-7 в deferred/cancelled, без активной разработки.", "checked": True},
{"text": "Не использовать старое HTML-тело карточки как актуальный UI-спек.", "checked": True},
],
),
text_block(
"design-guide",
"Реализация этапа 1",
"""
Карточка приведена к короткой structured layout записи. Исторический смысл сохранен, но актуальный дизайн-канон вынесен в документ репозитория, чтобы UI-решения не расходились между карточкой, кодом и документацией.
""",
),
text_block(
"design-guide",
"Этап 2. Future design-system extraction",
"""
Статус: backlog/deferred.
Если появится необходимость переиспользовать NODE.DC UI между несколькими приложениями как библиотеку, эту работу нужно открывать отдельным этапом внутри той же крупной карточки: tokens, primitives, surfaces, modals, cards, auth shell и cross-app visual QA.
""",
),
checker(
"design-guide2",
"Чекер этапа 2. Future design-system extraction",
[
"Принять решение, нужен ли отдельный design-system package.",
"Выделить переиспользуемые primitives без ломки Launcher/Tasker UI.",
"Описать visual QA для Auth/Launcher/Tasker экранов.",
],
),
],
},
{
"slug": "tasker-provisioning-workspace-onboarding",
"name": "Tasker provisioning и workspace onboarding",
"priority": "high",
"state_group": "started",
"assignees": [CODEX_EMAIL],
"description_html": html(
"Архитектурный узел между Launcher control plane и Operational Core: пользователь должен попадать в Tasker через NODE.DC SSO без redirect-loop, 500-страниц и ручных Plane-инвайтов.",
"Цель: разделить доступ к приложению, workspace onboarding и доменные роли Tasker. Launcher выдает сервисный доступ и enterprise-назначения, Tasker сохраняет собственные workspace/project/task модели и standalone-режим.",
"Критерий приемки: пользователь с доступом к Operational Core получает предсказуемый исход: назначенный workspace/project, разрешенный self-service workspace или ожидание назначения; для managedBy=launcher нет второго источника прав внутри Tasker.",
),
"blocks": [
text_block(
"tasker-provisioning",
"Текущая архитектура",
"""
Launcher уже является рабочим control plane для клиентов, пользователей, групп, инвайтов, service grants, Operational Core workspace binding и project-level назначений. Authentik получает техническую group projection из Launcher и остается внутренним IdP.
Operational Core подключен как Plane fork: OIDC/handoff, ExternalIdentityLink, live access middleware, workspace policy hook и internal adapter endpoints живут в Tasker, а Launcher вызывает их через защищенный server-to-server API. Прямых записей Launcher в Plane DB нет.
Фактически реализованы: auto-create/link локального Tasker user по verified OIDC/handoff claims, workspace membership bridge, project membership bridge, policy check на создание workspace, NODE.DC create-workspace UX и очистка stale assignees после снятия пользователей из workspace/project.
Открытая архитектурная граница: нужно формально закрепить managedBy=launcher/managedBy=tasker для workspace, чтобы enterprise-workspace управлялись из Launcher, а standalone/public workspace сохраняли штатные Tasker users/invites/admin mechanics.
""",
),
text_block(
"tasker-provisioning",
"Этап 1. OIDC fail-safe и local user/link provisioning",
"""
Статус: реализовано.
Этап закрывает патологию первого входа: Authentik подтверждает пользователя, но Tasker не знает локального User/ExternalIdentityLink. Сейчас Tasker умеет idempotently создать или связать локального пользователя по verified OIDC/handoff claims и не уводит пользователя в бесконечный login loop.
""",
),
checker(
"tasker-provisioning1",
"Чекер этапа 1. OIDC fail-safe и local user/link provisioning",
[
{"text": "Развести ошибки: нет platform access, нет local user/link, нет workspace membership.", "checked": True},
{"text": "Добавить auto-link existing Plane user по email отдельным env-флагом.", "checked": True},
{"text": "Добавить auto-create Tasker user по verified OIDC claims отдельным env-флагом.", "checked": True},
{"text": "Создавать/обновлять ExternalIdentityLink provider=authentik + subject.", "checked": True},
{"text": "Синхронизировать display name/email/avatar без пересоздания старого Plane user.", "checked": True},
{"text": "Сохранить standalone Plane auth/API механизмы вне NODE.DC env-профиля.", "checked": True},
],
),
text_block(
"tasker-provisioning",
"Реализация этапа 1",
"""
Tasker OIDC слой живет в plane-src/apps/api/plane/authentication/views/app/oidc.py. Он проверяет state/nonce/JWKS/audience/issuer, принимает groups, резолвит или создает локального пользователя и обновляет ExternalIdentityLink.
Launcher service handoff ведет в /auth/nodedc/handoff/ и потребляется через защищенный /api/internal/handoff/consume. Handoff передает normalized user, avatarUrl и groups без раскрытия пароля или Authentik service token.
Профильная синхронизация идет из claims и Launcher avatar URL. Существующий Plane owner dcctouch@gmail.com не пересоздается: связь держится через ExternalIdentityLink и старый Plane user сохраняет workspace/project/task связи.
""",
),
text_block(
"tasker-provisioning",
"Этап 2. Launcher -> Tasker workspace adapter",
"""
Статус: реализовано.
Этап добавляет управляемый bridge для enterprise workspace: Launcher может назначить или снять пользователя в Operational Core workspace через internal Tasker API, а Tasker idempotently меняет WorkspaceMember без Plane email-invite.
""",
),
checker(
"tasker-provisioning2",
"Чекер этапа 2. Launcher -> Tasker workspace adapter",
[
{"text": "Добавить Tasker internal workspace catalog endpoint.", "checked": True},
{"text": "Добавить internal endpoint ensure workspace membership.", "checked": True},
{"text": "Добавить internal endpoint remove workspace membership.", "checked": True},
{"text": "Защитить endpoints внутренним token/secret.", "checked": True},
{"text": "Добавить Launcher admin routes для workspace membership.", "checked": True},
{"text": "Сохранять workspace membership projection в Launcher storage.", "checked": True},
{"text": "Не использовать Plane email-invite как основной enterprise flow.", "checked": True},
],
),
text_block(
"tasker-provisioning",
"Реализация этапа 2",
"""
Tasker adapter endpoints лежат в plane-src/apps/api/plane/authentication/views/nodedc_workspace_adapter.py и подключены в plane-src/apps/api/plane/urls.py. Они резолвят workspace/user по slug/id/email/subject, создают или деактивируют WorkspaceMember и возвращают normalized membership summary.
Launcher BFF вызывает adapter из server/dev-server.mjs, проверяет права администратора на client/user, пишет projection через server/control-plane-store.mjs и обновляет UI через admin overlay.
""",
),
text_block(
"tasker-provisioning",
"Этап 3. Workspace onboarding policy",
"""
Статус: частично реализовано.
Сервисный доступ к Operational Core и право создавать workspace разделены. Launcher уже возвращает workspacePolicy через internal access check, а Tasker спрашивает policy перед create-workspace flow. Полная трехрежимная модель client/user/group policy еще не оформлена как production data model.
""",
),
checker(
"tasker-provisioning3",
"Чекер этапа 3. Workspace onboarding policy",
[
{"text": "Добавить Launcher setting taskManager.workspaceCreationPolicy.", "checked": True},
{"text": "Возвращать workspacePolicy из /api/internal/access/check.", "checked": True},
{"text": "Добавить Tasker backend workspace policy resolver.", "checked": True},
{"text": "Сохранить self-service create-workspace flow в NODE.DC дизайне.", "checked": True},
"Оформить admin_managed ожидание назначения без create workspace.",
"Добавить production policy model на уровне client/service/user/group.",
"Связать policy с managedBy=launcher/managedBy=tasker.",
],
),
text_block(
"tasker-provisioning",
"Реализация этапа 3",
"""
Launcher хранит базовую настройку workspace creation policy в settings.taskManager. Tasker использует plane.authentication.nodedc_workspace_policy, чтобы получить решение из Launcher access-check endpoint и разрешить или запретить создание workspace.
Create workspace UI в Tasker приведен к NODE.DC auth-card layout без удаления штатной Plane формы: вариант nodedc-auth сохраняет standalone совместимость и не ломает остальные вызовы.
""",
),
text_block(
"tasker-provisioning",
"Этап 4. Project-level доступы Operational Core из Launcher",
"""
Статус: реализовано локально, ожидает ручной acceptance.
Этап закрывает следующий слой после workspace binding: Launcher может назначать роль пользователя внутри конкретного проекта Tasker. Это остается adapter-вызовом через internal API, а не прямой записью в Plane DB.
""",
),
checker(
"tasker-provisioning4",
"Чекер этапа 4. Project-level access bridge",
[
{"text": "Расширить Tasker workspace catalog проектами внутри workspace.", "checked": True},
{"text": "Добавить internal endpoint ensure project membership.", "checked": True},
{"text": "Добавить internal endpoint remove project membership.", "checked": True},
{"text": "Сохранять project membership projection в Launcher storage.", "checked": True},
{"text": "Показать workspaces и projects в Operational Core modal.", "checked": True},
{"text": "При project role гарантировать минимальный workspace membership.", "checked": True},
"Провести ручной acceptance: назначение роли проекта, refresh, вход пользователем в Tasker.",
],
),
text_block(
"tasker-provisioning",
"Реализация этапа 4",
"""
Tasker: добавлены endpoints /api/internal/nodedc/project-memberships/ensure/ и /api/internal/nodedc/project-memberships/remove/. Они защищены тем же internal token, резолвят workspace/project/user и idempotently создают или обновляют ProjectMember.
Launcher: добавлены admin routes для project memberships, control-plane projection taskManagerProjectMemberships и UI в Operational Core modal. Роль проекта меняется кликом: прочерк, гость, участник, админ.
Проверки этапа ранее проходили: npm run build в Launcher, node --check server/dev-server.mjs server/control-plane-store.mjs и python compile для Tasker adapter/routes.
""",
),
text_block(
"tasker-provisioning",
"Этап 5. Stale assignees cleanup после снятия пользователей",
"""
Статус: реализовано в рабочем дереве, ожидает финальную проверку и коммит после подтверждения.
После удаления, блокировки или снятия пользователя из workspace/project Tasker не должен продолжать показывать его исполнителем в карточках и группировках. Последняя локальная правка удаляет IssueAssignee на membership remove и фильтрует assignee arrays только по активным workspace/project membership.
""",
),
checker(
"tasker-provisioning5",
"Чекер этапа 5. Stale assignees cleanup",
[
{"text": "Удалять IssueAssignee при снятии workspace membership.", "checked": True},
{"text": "Удалять IssueAssignee при снятии project membership.", "checked": True},
{"text": "Покрыть admin/license/member remove paths.", "checked": True},
{"text": "Фильтровать backend assignee_ids по active workspace/project membership.", "checked": True},
{"text": "Добавить frontend guard в internal Kanban card.", "checked": True},
{"text": "Проверить текущую БД на реально stale IssueAssignee.", "checked": True},
"Прогнать целевой regression по issue list/kanban после restart backend/web.",
],
),
text_block(
"tasker-provisioning",
"Реализация этапа 5",
"""
Затронуты Tasker файлы: plane-src/apps/api/plane/authentication/views/nodedc_workspace_adapter.py, workspace/project/member/license views, issue list/detail endpoints, common grouper utilities и internal-contour-card.tsx.
Проверка БД 2026-05-09: запрос по активным IssueAssignee без active WorkspaceMember/ProjectMember вернул 0 записей. Значит текущий runtime не содержит реально stale assignee links после последней очистки.
Рабочее дерево Task Manager остается dirty: этот этап еще не закоммичен и требует финальной проверки перед переводом карточного пункта в полностью закрытое состояние.
""",
),
text_block(
"tasker-provisioning",
"Этап 6. Source-of-truth split managedBy",
"""
Статус: следующий критический этап.
Нужно формально закрепить источник управления для workspace. managedBy=launcher означает enterprise workspace: пользователи, инвайты и базовые роли идут из Launcher, а Tasker не должен давать конфликтующее управление. managedBy=tasker означает standalone/public workspace: штатные Tasker механизмы пользователей, инвайтов и ролей остаются включенными.
""",
),
checker(
"tasker-provisioning6",
"Чекер этапа 6. Source-of-truth split managedBy",
[
"Добавить managedBy в Launcher Tasker workspace binding.",
"Возвращать managedBy/workspacePolicy из Launcher internal access-check.",
"Передавать managedBy в Tasker adapter responses или workspace policy resolver.",
"Скрыть или readonly-заблокировать Tasker users/invites для managedBy=launcher.",
"Оставить Tasker users/invites включенными для managedBy=tasker.",
"Проверить enterprise client admin и public self-service user flows отдельно.",
"Зафиксировать правила в NDCPLATFORM-4 и NDCPLATFORM-10 после реализации.",
],
),
],
},
{
"slug": "safari-workspace-crash-debug",
"name": "Safari: workspace crash и storage/OIDC debug",
"priority": "high",
"state_group": "cancelled",
"assignees": [CODEX_EMAIL],
"description_html": html(
"Отложенный debug Safari-only падения workspace в Task Manager. Проблема не блокирует текущую платформенную архитектуру, потому что Chrome/Chromium flow работает.",
"Цель актуализации: сохранить симптом и границы диагностики, но не смешивать browser-specific bug с Launcher/Auth/Tasker source-of-truth работами.",
"Статус: deferred. Возвращаться после закрытия критичного managedBy/source-of-truth этапа или при воспроизводимом Safari regression report.",
),
"blocks": [
text_block(
"safari-crash",
"Текущая архитектура",
"""
Safari-only падение workspace относится к browser/runtime compatibility слою Task Manager. Оно не меняет целевую архитектуру: Authentik остается внутренним IdP, Launcher source of truth для enterprise-доступов, Task Manager standalone-capable Operational Core module.
Пока Chrome/Chromium flow работает, этот debug не должен блокировать платформенную работу по access matrix, workspace onboarding и managedBy split. Карточка остается отложенной, чтобы не смешивать runtime browser bug с архитектурными задачами.
""",
),
text_block(
"safari-crash",
"Этап 1. Сохранить симптом и границы debug",
"""
Статус: зафиксировано как deferred.
Нужно вернуться к карточке только при наличии воспроизводимого сценария: Safari version, URL, user, workspace slug, console trace, network trace, storage/cookie state и отличие от Chrome на том же аккаунте.
""",
),
checker(
"safari1",
"Чекер этапа 1. Deferred symptom",
[
{"text": "Не считать Safari-only падение блокером Launcher/Auth/Tasker архитектуры.", "checked": True},
{"text": "Сохранить карточку как отдельный debug bucket.", "checked": True},
"Снять Safari console trace.",
"Снять Safari network trace.",
"Сравнить localStorage/sessionStorage/cookies Safari vs Chrome.",
],
),
text_block(
"safari-crash",
"Этап 2. Future Safari diagnostics",
"""
Статус: deferred.
Будущий debug должен идти от воспроизводимого crash-path: auth/session sync, workspace bootstrap API, storage hydration, frontend route boundary, browser-specific cookie policy или WebKit-only JS/runtime issue.
""",
),
checker(
"safari2",
"Чекер этапа 2. Safari diagnostics",
[
"Воспроизвести падение на актуальном Safari.",
"Отделить auth/session issue от frontend runtime crash.",
"Проверить WebKit cookie/storage policy на task.local.nodedc.",
"Сформулировать минимальный фикс без изменения общей архитектуры.",
],
),
],
},
{
"slug": "public-user-entry-and-service-access",
"name": "Публичный контур пользователей",
"priority": "high",
"state_group": "backlog",
"assignees": [PLATFORM_OWNER_EMAIL, CODEX_EMAIL],
"description_html": html(
"Отдельный public/open-access контур для внешних пользователей, которые приходят не как участники клиентской компании, а как самостоятельные пользователи сервисов NODE.DC.",
"Цель: не плодить новый админский интерфейс, а расширить существующий Launcher control plane. Enterprise-компании остаются company-scoped, public users попадают в отдельный Public Access Pool и получают сервисные доступы вручную через root-admin flow.",
"Критерий приемки: пользователь может нажать Запросить доступ в Auth/Login, заявка появляется в Launcher в разделе Инвайты как вкладка Заявки, root admin вручную апрувит/отклоняет, генерирует ссылку, а после входа пользователь видит только витрину и свои разрешенные сервисы.",
),
"blocks": [
text_block(
"public-users",
"Текущая архитектура",
"""
Уже есть единый Authentik login, branded под NODE.DC, и Launcher как точка входа. Launcher умеет показывать витрину приложений, хранить клиентов, пользователей, инвайты, группы, матрицу доступов и привязку Operational Core workspace к клиенту.
Enterprise-сценарий частично реализован: root admin создает компанию, добавляет туда client admin, выдает доступы, а client admin дальше управляет пользователями, группами, инвайтами и назначениями в рамках своей компании через Launcher. В Operational Core такие пользователи не должны видеть управление users/invites/workspace creation для launcher-managed workspace; они работают только в среде, которую им задал админ.
Public/open-access контур должен быть отдельной группой пользователей, не привязанной к клиентской компании. Рабочее название: Public Access Pool / Свободный доступ. Это не новый enterprise-client и не хаотичный список отдельных компаний, а отдельный cohort в Launcher, видимый root admin. Public user не получает Launcher admin overlay, пока ему явно не выдали роль; он видит витрину и сервисы.
Глобальная identity должна быть одна на email/Auth subject. Если пользователь сначала попал в Public Access Pool, а потом его надо перевести в enterprise-компанию, правильная операция не удаление и повторная регистрация, а административный перевод: создать company membership, выбрать роль/группы, при необходимости убрать public-cohort marker и пересчитать доступы.
Также появился standalone-сценарий Operational Core: public-пользователь после выдачи доступа к сервису может создать собственный workspace и управлять им через штатный Task Manager, но только для workspace managedBy=tasker. Создание NODE.DC identity и первичная выдача сервисного доступа остаются в Launcher.
Еще не реализованы: кнопка Запросить доступ в login flow, модель access requests с обязательными полями, вкладка Заявки рядом с текущими Инвайтами, Public Access Pool в Launcher admin, перевод public user в компанию, правила приглашения других людей в public Tasker workspace без почтовой инфраструктуры.
""",
),
text_block(
"public-users",
"Этап 1. Разделение enterprise и Public Access Pool",
"""
Статус: backlog.
Нужно формально разделить два режима: enterprise/direct-contract company scope и public/open-access pool. Enterprise-контур остается текущей реализацией: root admin создает компанию, назначает client admin, client admin управляет только своей компанией через Launcher.
Public Access Pool отдельный cohort для внешних самостоятельных пользователей. Он должен быть доступен root admin в существующем Launcher admin UI как отдельный режим/область рядом с компаниями, но не должен засорять список компаний сотнями псевдо-клиентов.
Главное правило безопасности: у каждого Operational Core workspace должен быть ровно один источник управления правами. managedBy=launcher означает управление из Launcher и readonly/hidden member management в Tasker; managedBy=tasker означает штатное workspace management внутри Operational Core, но identity/service entitlement все равно выдаются через Launcher.
""",
),
checker(
"public-users1",
"Чекер этапа 1. Разделение enterprise и Public Access Pool",
[
"Зафиксировать типы контуров: enterprise/direct-contract и public/open-access.",
"Добавить модель Public Access Pool или эквивалентный user cohort без создания отдельной компании на каждого пользователя.",
"Сделать global user уникальным по email/Auth subject независимо от public или enterprise происхождения.",
"Показать Public Access Pool root admin в существующем Launcher admin selector/режиме.",
"Скрыть Public Access Pool от client admin компаний.",
"Описать managedBy=launcher для корпоративных workspace.",
"Описать managedBy=tasker для публичных standalone workspace.",
"Развести видимость админки Launcher для root admin, client admin и public user.",
"Описать, как public-пользователь видит витрину сервисов без администрирования Launcher.",
"Зафиксировать запрет прямого создания Auth/Launcher users из Tasker без Launcher approval.",
],
),
text_block(
"public-users",
"Этап 2. Запрос приглашения из окна входа",
"""
Статус: backlog.
В branded login нужно добавить безопасный сценарий запроса доступа. Это не регистрация с паролем и не обход Authentik: пользователь оставляет заявку, а учетная запись и invite создаются только после ручного решения root admin.
Заявка должна попадать в Launcher admin, а не теряться в почте. Почтовое уведомление можно добавить позже как транспорт, но source of truth для обработки заявок должен быть внутри Launcher.
UI-решение: не добавлять новое большое окно. Расширить существующий раздел Инвайты вкладками Заявки и Сгенерированные инвайты. Для root admin вкладка Заявки показывает public/open-access заявки. Для client admin текущие company-scoped инвайты остаются без доступа к global public queue.
""",
),
checker(
"public-users2",
"Чекер этапа 2. Запрос приглашения из окна входа",
[
"Добавить вторичную кнопку Запросить доступ под кнопкой Войти.",
"Добавить форму заявки с обязательными полями: email, имя, фамилия, отчество, телефон, компания.",
"Оставить интересующий сервис/задачу и комментарий опциональными полями.",
"Блокировать отправку, если любое обязательное поле пустое.",
"Дублировать required validation server-side в Launcher public endpoint.",
"Не запрашивать пароль на этапе заявки.",
"Сохранять заявку server-side в Launcher storage/backend как accessRequest.",
"Расширить текущий раздел Инвайты вкладками Заявки и Сгенерированные инвайты.",
"Добавить статусы заявки: новая, в работе, принята, отклонена, архив.",
"Добавить действие Принять: создать invite в Public Access Pool для указанного email.",
"Добавить действие Отклонить без создания пользователя/Auth identity.",
"Пока нет почты, показывать сгенерированную ссылку для ручной передачи админом.",
],
),
text_block(
"public-users",
"Этап 3. Public user access flow",
"""
Статус: backlog.
После апрува пользователь регистрируется по invite link, попадает в Launcher и видит витрину сервисов. Администрирования Launcher у него нет. Он находится в Public Access Pool, но не получает доступ к Operational Core или другим сервисам автоматически.
Доступы к сервисам на первом этапе выдает root admin вручную через матрицу доступов. Когда public user получает Operational Core, он может создать собственный workspace в Tasker. Такой workspace должен быть managedBy=tasker: Tasker управляет проектами и workspace membership, Launcher управляет только identity, service entitlement и глобальной блокировкой пользователя.
""",
),
checker(
"public-users3",
"Чекер этапа 3. Public user access flow",
[
"Проверить, что public user не видит администрирование Launcher.",
"Проверить, что public user после invite попадает в Public Access Pool.",
"Не выдавать Operational Core автоматически только по факту принятого invite.",
"Показывать все сервисы витрины с состояниями доступен/нет доступа.",
"Раздавать сервисный доступ public user через матрицу доступов Launcher.",
"Открывать Operational Core без повторной авторизации после доступа.",
"Разрешить создание workspace внутри Tasker для managedBy=tasker.",
"Оставить настройки workspace/project внутри Tasker включенными для managedBy=tasker.",
"Скрывать настройки участников/инвайтов Tasker только для managedBy=launcher.",
],
),
text_block(
"public-users",
"Этап 4. Перевод Public user в enterprise-компанию",
"""
Статус: backlog.
Нужна штатная операция root admin: перевести пользователя из Public Access Pool в конкретную компанию. Это безопаснее, чем удалять пользователя и заставлять его регистрироваться заново по enterprise-инвайту, потому что Authentik identity, session history, audit trail, accepted invites и будущие billing/usage links остаются консистентными.
Целевая модель: Launcher хранит global user отдельно от company memberships. Public Access Pool cohort/source/status, а не единственный контейнер identity. При переводе root admin выбирает компанию, роль, группы и политику доступа. Система создает или обновляет ClientMembership, может архивировать public pool membership/request, пересчитывает Authentik groups и service grants. Если enterprise admin позже выписывает invite на email уже существующего public user, accept flow должен merge by email в тот же global user, а не создавать дубль.
Нужна также мягкая операция Убрать из Public Pool. Она не должна по умолчанию удалять global user/Auth identity: она архивирует public cohort и отзывает public-only grants. Полное удаление пользователя отдельное destructive действие с явным cleanup-планом.
""",
),
checker(
"public-users4",
"Чекер этапа 4. Перевод Public user в компанию",
[
"Добавить root-admin действие Перевести в компанию для пользователя из Public Access Pool.",
"Выбирать target company, company role и группы при переводе.",
"Создавать или обновлять ClientMembership без создания второго global user.",
"Принимать enterprise invite на email существующего public user через merge by email.",
"Архивировать или отключать public cohort marker после успешного перевода по выбору admin.",
"Пересчитывать Authentik groups/service grants после перевода.",
"Логировать перевод в audit с source pool, target company, actor и ролью.",
"Добавить действие Убрать из Public Pool без удаления global Auth identity по умолчанию.",
"Оставить полное удаление user отдельным destructive flow с подтверждением и cleanup.",
],
),
text_block(
"public-users",
"Этап 5. Public workspace collaboration и invite boundary",
"""
Статус: backlog.
Самый опасный участок как public-пользователи приглашают других людей в свой Operational Core workspace без готовой почтовой инфраструктуры. Нельзя позволять Tasker напрямую создавать Authentik/Launcher users: это ломает единый identity perimeter.
Правило для NODE.DC public mode: если приглашенный email уже принадлежит активному NODE.DC user с доступом к Operational Core, Tasker может добавить его в managedBy=tasker workspace по точному email/ID без глобального поиска людей. Если пользователя нет или у него нет entitlement на Operational Core, Tasker должен создать request в Launcher с контекстом workspace/requestedBy/targetEmail/role. Root admin вручную апрувит, генерирует invite link и после принятия доступ доводится до нужного workspace через Tasker adapter.
Для managedBy=launcher workspace любые Tasker-side invites/users остаются скрытыми или readonly. Приглашения в enterprise-контуре идут через Launcher company admin/root admin, как в текущей реализации.
""",
),
checker(
"public-users5",
"Чекер этапа 5. Public workspace collaboration",
[
"Запретить прямое создание Authentik/Launcher user из Tasker invite flow.",
"Разрешить добавление существующего NODE.DC user в public managedBy=tasker workspace по точному email/ID.",
"Проверять, что target user имеет Operational Core entitlement перед прямым добавлением.",
"Если target user отсутствует или не имеет entitlement, создавать Launcher accessRequest с Tasker workspace context.",
"Показывать такие заявки root admin во вкладке Заявки с источником Operational Core.",
"После approval создавать invite link и связывать accepted user с исходным Tasker workspace.",
"Оставить enterprise managedBy=launcher workspace без Tasker-side invites.",
"Отложить автоматическую email-доставку до отдельного mailer/email этапа.",
],
),
text_block(
"public-users",
"Этап 6. Billing-ready модель",
"""
Статус: backlog.
Биллинг пока не реализуется, но модель публичного контура должна не закрыть путь к оплатам. Для enterprise-клиентов остаются прямые договоры и ручные даты договора/оплаты. Для public/self-service пользователей позже появятся подписки, тарифы, лимиты и модульные entitlements.
""",
),
checker(
"public-users6",
"Чекер этапа 6. Billing-ready модель",
[
"Описать entitlement по каждому сервису отдельно.",
"Описать статус подписки public user: trial, active, expired, blocked.",
"Заложить лимиты Operational Core: workspace count, members count, storage.",
"Заложить лимиты Voice Tasker: minutes, requests, model tier.",
"Не внедрять платежный провайдер до отдельного billing RFC.",
"Не смешивать прямые договоры enterprise-клиентов и public subscriptions.",
],
),
],
},
{
@ -1073,6 +1651,7 @@ def ensure_issue(workspace, project, codex_user, spec):
issue.target_date = None
issue.external_source = SOURCE
issue.external_id = spec["slug"]
issue.created_by = issue.created_by or codex_user
issue.updated_by = codex_user
issue.save(disable_auto_set_user=True)