From ca9fd34e9127c9acfa84c235947be70fccdbba3b Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Sat, 9 May 2026 12:33:54 +0300 Subject: [PATCH] =?UTF-8?q?=D0=90=D0=A0=D0=A5=20-=20=D0=9C=D0=95=D0=96?= =?UTF-8?q?=D0=9F=D0=A0=D0=9E=D0=95=D0=9A=D0=A2=D0=9D=D0=90=D0=AF=20=D0=9A?= =?UTF-8?q?=D0=9E=D0=9C=D0=9C=D0=A3=D0=9D=D0=98=D0=9A=D0=90=D0=A6=D0=98?= =?UTF-8?q?=D0=AF:=20=D0=B0=D0=BA=D1=82=D1=83=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20roadmap=20NDC=20platform?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/bootstrap_nodedc_platform_plan.py | 579 ++++++++++++++++++++++ 1 file changed, 579 insertions(+) diff --git a/scripts/bootstrap_nodedc_platform_plan.py b/scripts/bootstrap_nodedc_platform_plan.py index 6496eeb..c5056c4 100644 --- a/scripts/bootstrap_nodedc_platform_plan.py +++ b/scripts/bootstrap_nodedc_platform_plan.py @@ -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)