From 97566faba3759951ab7bd2abd970feafa5f61924 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Thu, 14 May 2026 20:49:52 +0300 Subject: [PATCH] =?UTF-8?q?FEAT=20-=20TASKER:=20=D0=BF=D0=BE=D0=BA=D0=B0?= =?UTF-8?q?=D0=B7=20Codex=20Agent=20API=20=D0=BF=D0=BE=20Launcher=20entitl?= =?UTF-8?q?ement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../authentication/nodedc_workspace_policy.py | 23 ++++ .../(workspace)/codex-agent-api/page.tsx | 12 ++ plane-src/apps/web/app/routes/core.ts | 4 + .../workspace/sidebar/item-categories.tsx | 37 +++++- .../settings/workspace/sidebar/item-icon.tsx | 3 +- .../settings/codex-agent-api-settings.tsx | 108 ++++++++++++++++++ .../settings/workspace-settings-modal.tsx | 37 +++++- .../workspace-settings-modal.utils.ts | 12 +- .../web/core/services/workspace.service.ts | 3 + .../constants/src/settings/workspace.ts | 12 +- .../i18n/src/locales/en/translations.ts | 34 +++--- .../i18n/src/locales/ru/translations.ts | 22 ++-- plane-src/packages/types/src/settings.ts | 3 +- 13 files changed, 276 insertions(+), 34 deletions(-) create mode 100644 plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/codex-agent-api/page.tsx create mode 100644 plane-src/apps/web/core/components/workspace/settings/codex-agent-api-settings.tsx diff --git a/plane-src/apps/api/plane/authentication/nodedc_workspace_policy.py b/plane-src/apps/api/plane/authentication/nodedc_workspace_policy.py index 697f99a..cbbd883 100644 --- a/plane-src/apps/api/plane/authentication/nodedc_workspace_policy.py +++ b/plane-src/apps/api/plane/authentication/nodedc_workspace_policy.py @@ -25,6 +25,7 @@ def get_nodedc_workspace_creation_policy(user, workspace_slug=None): "default_managed_by": "tasker", "invite_approval": "tasker", "default_invite_approval": "tasker", + "service_modules": {}, "workspaces": [], "reason": "NODE.DC workspace policy is not configured.", } @@ -45,6 +46,7 @@ def get_nodedc_workspace_creation_policy(user, workspace_slug=None): "default_managed_by": "tasker", "invite_approval": "tasker", "default_invite_approval": "tasker", + "service_modules": {}, "workspaces": [], "reason": "NODE.DC identity is not linked." if enforce_unlinked else "Standalone user without NODE.DC identity.", } @@ -75,11 +77,18 @@ def get_nodedc_workspace_creation_policy(user, workspace_slug=None): "default_managed_by": "tasker", "invite_approval": "disabled", "default_invite_approval": "tasker", + "service_modules": {}, "workspaces": [], "reason": "NODE.DC workspace policy is unavailable.", } workspace_policy = payload.get("workspacePolicy") if isinstance(payload.get("workspacePolicy"), dict) else {} + service_modules = normalize_service_modules( + workspace_policy.get("serviceModules") + or workspace_policy.get("service_modules") + or payload.get("serviceModules") + or payload.get("service_modules") + ) access_allowed = bool(payload.get("allowed")) if not workspace_policy: return { @@ -90,6 +99,7 @@ def get_nodedc_workspace_creation_policy(user, workspace_slug=None): "default_managed_by": "tasker", "invite_approval": "tasker", "default_invite_approval": "tasker", + "service_modules": service_modules, "workspaces": [], "reason": payload.get("reason") or "NODE.DC access check does not expose workspace policy.", } @@ -117,6 +127,7 @@ def get_nodedc_workspace_creation_policy(user, workspace_slug=None): "default_managed_by": normalize_managed_by(workspace_policy.get("defaultManagedBy") or workspace_policy.get("managedBy")), "invite_approval": invite_approval, "default_invite_approval": default_invite_approval, + "service_modules": service_modules, "workspaces": workspaces, "reason": workspace_policy.get("reason") or payload.get("reason") or "NODE.DC workspace policy decision.", } @@ -159,6 +170,18 @@ def normalize_workspace_management_list(value): return workspaces +def normalize_service_modules(value): + if not isinstance(value, dict): + return {} + + service_modules = {} + for module_key in ("codex_agents",): + if value.get(module_key) is True: + service_modules[module_key] = True + + return service_modules + + def resolve_workspace_managed_by(workspace_slug, workspaces, fallback): if isinstance(workspace_slug, str) and workspace_slug.strip(): normalized_slug = workspace_slug.strip() diff --git a/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/codex-agent-api/page.tsx b/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/codex-agent-api/page.tsx new file mode 100644 index 0000000..fd062ef --- /dev/null +++ b/plane-src/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/codex-agent-api/page.tsx @@ -0,0 +1,12 @@ +import { redirect } from "react-router"; +// local imports +import type { Route } from "./+types/page"; + +export function clientLoader({ params }: Route.ClientLoaderArgs) { + const { workspaceSlug } = params; + throw redirect(`/${workspaceSlug}/?workspaceSettings=codex-agent-api`); +} + +export default function CodexAgentApiSettingsPage() { + return null; +} diff --git a/plane-src/apps/web/app/routes/core.ts b/plane-src/apps/web/app/routes/core.ts index 929da63..787fd55 100644 --- a/plane-src/apps/web/app/routes/core.ts +++ b/plane-src/apps/web/app/routes/core.ts @@ -297,6 +297,10 @@ export const coreRoutes: RouteConfigEntry[] = [ ":workspaceSlug/settings/ai-voice-tasker", "./(all)/[workspaceSlug]/(settings)/settings/(workspace)/ai-voice-tasker/page.tsx" ), + route( + ":workspaceSlug/settings/codex-agent-api", + "./(all)/[workspaceSlug]/(settings)/settings/(workspace)/codex-agent-api/page.tsx" + ), ]), // -------------------------------------------------------------------- diff --git a/plane-src/apps/web/core/components/settings/workspace/sidebar/item-categories.tsx b/plane-src/apps/web/core/components/settings/workspace/sidebar/item-categories.tsx index 382b253..71ce94a 100644 --- a/plane-src/apps/web/core/components/settings/workspace/sidebar/item-categories.tsx +++ b/plane-src/apps/web/core/components/settings/workspace/sidebar/item-categories.tsx @@ -9,7 +9,12 @@ import { usePathname } from "next/navigation"; import { useParams } from "react-router"; import useSWR from "swr"; // plane imports -import { EUserPermissionsLevel, GROUPED_WORKSPACE_SETTINGS, WORKSPACE_SETTINGS, WORKSPACE_SETTINGS_CATEGORIES } from "@plane/constants"; +import { + EUserPermissionsLevel, + GROUPED_WORKSPACE_SETTINGS, + WORKSPACE_SETTINGS, + WORKSPACE_SETTINGS_CATEGORIES, +} from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import type { TWorkspaceSettingsTabs } from "@plane/types"; import { joinUrlPath } from "@plane/utils"; @@ -19,12 +24,14 @@ import { SettingsSidebarItem } from "@/components/settings/sidebar/item"; import { useUserPermissions } from "@/hooks/store/user"; // services import { WorkspaceAIService } from "@/services/workspace-ai.service"; +import { WorkspaceService } from "@/services/workspace.service"; // local imports import { WORKSPACE_SETTINGS_ICONS } from "./item-icon"; const HIDDEN_WORKSPACE_SETTINGS_KEYS = new Set(["billing-and-plans"]); -const WORKSPACE_FEATURE_GATED_SETTINGS_KEYS = new Set(["ai-voice-tasker"]); +const WORKSPACE_FEATURE_GATED_SETTINGS_KEYS = new Set(["ai-voice-tasker", "codex-agent-api"]); const workspaceAIService = new WorkspaceAIService(); +const workspaceService = new WorkspaceService(); export const WorkspaceSettingsSidebarItemCategories = observer(function WorkspaceSettingsSidebarItemCategories() { // params @@ -42,7 +49,12 @@ export const WorkspaceSettingsSidebarItemCategories = observer(function Workspac canLoadVoiceTaskerEntitlement ? `WORKSPACE_AI_SETTINGS_${workspaceSlug}` : null, () => workspaceAIService.retrieveSettings(workspaceSlug as string) ); + const { data: nodedcWorkspacePolicy } = useSWR( + workspaceSlug ? `NODEDC_WORKSPACE_POLICY_${workspaceSlug}` : null, + () => workspaceService.getNodeDCWorkspacePolicy(workspaceSlug as string) + ); const isVoiceTaskerEntitled = aiSettings?.feature_entitlement_enabled === true; + const isCodexAgentEntitled = nodedcWorkspacePolicy?.service_modules?.codex_agents === true; return (
@@ -51,7 +63,11 @@ export const WorkspaceSettingsSidebarItemCategories = observer(function Workspac const accessibleItems = categoryItems.filter( (item) => !HIDDEN_WORKSPACE_SETTINGS_KEYS.has(item.key) && - (!WORKSPACE_FEATURE_GATED_SETTINGS_KEYS.has(item.key) || isVoiceTaskerEntitled) && + (!WORKSPACE_FEATURE_GATED_SETTINGS_KEYS.has(item.key) || + isWorkspaceFeatureSettingsEntitled(item.key, { + isCodexAgentEntitled, + isVoiceTaskerEntitled, + })) && allowPermissions(item.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug) ); @@ -59,7 +75,7 @@ export const WorkspaceSettingsSidebarItemCategories = observer(function Workspac return (
-
+
{t(category)}
@@ -87,3 +103,16 @@ export const WorkspaceSettingsSidebarItemCategories = observer(function Workspac
); }); + +function isWorkspaceFeatureSettingsEntitled( + itemKey: TWorkspaceSettingsTabs, + entitlements: { + isCodexAgentEntitled: boolean; + isVoiceTaskerEntitled: boolean; + } +) { + if (itemKey === "ai-voice-tasker") return entitlements.isVoiceTaskerEntitled; + if (itemKey === "codex-agent-api") return entitlements.isCodexAgentEntitled; + + return true; +} diff --git a/plane-src/apps/web/core/components/settings/workspace/sidebar/item-icon.tsx b/plane-src/apps/web/core/components/settings/workspace/sidebar/item-icon.tsx index 54e484d..0903ebd 100644 --- a/plane-src/apps/web/core/components/settings/workspace/sidebar/item-icon.tsx +++ b/plane-src/apps/web/core/components/settings/workspace/sidebar/item-icon.tsx @@ -5,7 +5,7 @@ */ import type { LucideIcon } from "lucide-react"; -import { ArrowUpToLine, Building, CreditCard, Database, Mic, Users, Webhook } from "lucide-react"; +import { ArrowUpToLine, Bot, Building, CreditCard, Database, Mic, Users, Webhook } from "lucide-react"; // plane imports import type { ISvgIcons } from "@plane/propel/icons"; import type { TWorkspaceSettingsTabs } from "@plane/types"; @@ -18,4 +18,5 @@ export const WORKSPACE_SETTINGS_ICONS: Record workspaceService.getNodeDCWorkspacePolicy(workspaceSlug) + ); + const isCodexAgentEntitled = nodedcWorkspacePolicy?.service_modules?.codex_agents === true; + + return ( +
+ {showHeading && ( + + )} + + {isLoading ? ( +
Загрузка статуса модуля...
+ ) : ( + <> +
+
+
+
+ + Agent Gateway для {currentWorkspace?.name ?? workspaceSlug} +
+

+ Доступ к модулю приходит из Launcher entitlement Operational Core → Codex Agent API. Если entitlement + снят, этот раздел исчезает из настроек workspace и backend policy больше не возвращает активный модуль. +

+
+
+ + + + {isCodexAgentEntitled ? "Доступ выдан" : "Доступ не выдан"} +
+
+
+ +
+ + + +
+ + )} +
+ ); +}); + +type TCapabilityCardProps = { + description: string; + icon: typeof ShieldCheck; + title: string; +}; + +function CapabilityCard(props: TCapabilityCardProps) { + const Icon = props.icon; + + return ( +
+
+ + {props.title} +
+

{props.description}

+
+ ); +} diff --git a/plane-src/apps/web/core/components/workspace/settings/workspace-settings-modal.tsx b/plane-src/apps/web/core/components/workspace/settings/workspace-settings-modal.tsx index 73159ba..07ad48a 100644 --- a/plane-src/apps/web/core/components/workspace/settings/workspace-settings-modal.tsx +++ b/plane-src/apps/web/core/components/workspace/settings/workspace-settings-modal.tsx @@ -25,6 +25,7 @@ import { SettingsSidebarItem } from "@/components/settings/sidebar/item"; import { WORKSPACE_SETTINGS_ICONS } from "@/components/settings/workspace/sidebar/item-icon"; import { WorkspaceSettingsSidebarHeader } from "@/components/settings/workspace/sidebar/header"; import { AIVoiceTaskerSettingsContent } from "@/components/workspace/settings/ai-voice-tasker-settings"; +import { CodexAgentApiSettingsContent } from "@/components/workspace/settings/codex-agent-api-settings"; import { WorkspaceExportsSettingsContent } from "@/components/workspace/settings/exports-settings"; import { WorkspaceMembersSettingsContent } from "@/components/workspace/settings/members-settings"; import { StorageSettingsContent } from "@/components/workspace/settings/storage-settings"; @@ -48,7 +49,7 @@ import { const HIDDEN_WORKSPACE_SETTINGS_KEYS = new Set(["billing-and-plans"]); const LAUNCHER_MANAGED_HIDDEN_WORKSPACE_SETTINGS_KEYS = new Set(["members"]); -const WORKSPACE_FEATURE_GATED_SETTINGS_KEYS = new Set(["ai-voice-tasker"]); +const WORKSPACE_FEATURE_GATED_SETTINGS_KEYS = new Set(["ai-voice-tasker", "codex-agent-api"]); const MODAL_TABS = new Set([ "general", "members", @@ -56,6 +57,7 @@ const MODAL_TABS = new Set([ "storage", "webhooks", "ai-voice-tasker", + "codex-agent-api", ]); const workspaceAIService = new WorkspaceAIService(); const workspaceService = new WorkspaceService(); @@ -99,6 +101,7 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal() ); const isVoiceTaskerEntitled = aiSettings?.feature_entitlement_enabled === true; const isLauncherManagedWorkspace = nodedcWorkspacePolicy?.managed_by === "launcher"; + const isCodexAgentEntitled = nodedcWorkspacePolicy?.service_modules?.codex_agents === true; useEffect(() => { const syncFromLocation = () => { @@ -136,6 +139,11 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal() if (!isVoiceTaskerEntitled) openWorkspaceSettingsModal("general", true); }, [activeTab, isOpen, isVoiceTaskerEntitlementLoading, isVoiceTaskerEntitled]); + useEffect(() => { + if (!isOpen || activeTab !== "codex-agent-api" || !nodedcWorkspacePolicy) return; + if (!isCodexAgentEntitled) openWorkspaceSettingsModal("general", true); + }, [activeTab, isCodexAgentEntitled, isOpen, nodedcWorkspacePolicy]); + useEffect(() => { if (!isOpen || activeTab !== "members" || !isLauncherManagedWorkspace) return; openWorkspaceSettingsModal("general", true); @@ -162,6 +170,11 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal() return ; } + if (activeTab === "codex-agent-api" && currentWorkspace?.slug) { + if (!isCodexAgentEntitled) return ; + return ; + } + if (activeTab === "members" && currentWorkspace?.slug) { return ; } @@ -204,6 +217,7 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal() allowPermissions={allowPermissions} isVoiceTaskerEntitled={isVoiceTaskerEntitled} isLauncherManagedWorkspace={isLauncherManagedWorkspace} + isCodexAgentEntitled={isCodexAgentEntitled} workspaceSlug={currentWorkspace?.slug} />
@@ -238,6 +252,7 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal() type TWorkspaceModalSidebarProps = { activeTab: TWorkspaceSettingsModalTab; allowPermissions: ReturnType["allowPermissions"]; + isCodexAgentEntitled: boolean; isLauncherManagedWorkspace: boolean; isVoiceTaskerEntitled: boolean; onSelectItem: (itemKey: TWorkspaceSettingsTabs, itemHref: string) => void; @@ -247,6 +262,7 @@ type TWorkspaceModalSidebarProps = { function WorkspaceModalSidebar({ activeTab, allowPermissions, + isCodexAgentEntitled, isLauncherManagedWorkspace, isVoiceTaskerEntitled, onSelectItem, @@ -267,7 +283,11 @@ function WorkspaceModalSidebar({ (item) => !HIDDEN_WORKSPACE_SETTINGS_KEYS.has(item.key) && (!isLauncherManagedWorkspace || !LAUNCHER_MANAGED_HIDDEN_WORKSPACE_SETTINGS_KEYS.has(item.key)) && - (!WORKSPACE_FEATURE_GATED_SETTINGS_KEYS.has(item.key) || isVoiceTaskerEntitled) && + (!WORKSPACE_FEATURE_GATED_SETTINGS_KEYS.has(item.key) || + isWorkspaceFeatureSettingsEntitled(item.key, { + isCodexAgentEntitled, + isVoiceTaskerEntitled, + })) && allowPermissions(item.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug) ); @@ -302,3 +322,16 @@ function WorkspaceModalSidebar({ ); } + +function isWorkspaceFeatureSettingsEntitled( + itemKey: TWorkspaceSettingsTabs, + entitlements: { + isCodexAgentEntitled: boolean; + isVoiceTaskerEntitled: boolean; + } +) { + if (itemKey === "ai-voice-tasker") return entitlements.isVoiceTaskerEntitled; + if (itemKey === "codex-agent-api") return entitlements.isCodexAgentEntitled; + + return true; +} diff --git a/plane-src/apps/web/core/components/workspace/settings/workspace-settings-modal.utils.ts b/plane-src/apps/web/core/components/workspace/settings/workspace-settings-modal.utils.ts index 6ce8b09..6f40907 100644 --- a/plane-src/apps/web/core/components/workspace/settings/workspace-settings-modal.utils.ts +++ b/plane-src/apps/web/core/components/workspace/settings/workspace-settings-modal.utils.ts @@ -3,7 +3,14 @@ export const WORKSPACE_SETTINGS_MODAL_EVENT = "nodedc:workspace-settings-modal"; export const WORKSPACE_SETTINGS_WEBHOOK_QUERY_KEY = "webhookId"; -export type TWorkspaceSettingsModalTab = "general" | "members" | "export" | "storage" | "webhooks" | "ai-voice-tasker"; +export type TWorkspaceSettingsModalTab = + | "general" + | "members" + | "export" + | "storage" + | "webhooks" + | "ai-voice-tasker" + | "codex-agent-api"; type TWorkspaceSettingsModalEventDetail = { isOpen: boolean; @@ -23,7 +30,8 @@ export const getWorkspaceSettingsModalTabFromSearch = (search: string): TWorkspa value === "export" || value === "storage" || value === "webhooks" || - value === "ai-voice-tasker" + value === "ai-voice-tasker" || + value === "codex-agent-api" ) return value; diff --git a/plane-src/apps/web/core/services/workspace.service.ts b/plane-src/apps/web/core/services/workspace.service.ts index 828f288..f88b6e2 100644 --- a/plane-src/apps/web/core/services/workspace.service.ts +++ b/plane-src/apps/web/core/services/workspace.service.ts @@ -43,6 +43,9 @@ export interface NodeDCWorkspacePolicy { default_managed_by: "launcher" | "tasker"; invite_approval: "tasker" | "nodedc" | "launcher" | "disabled"; default_invite_approval: "tasker" | "nodedc" | "launcher" | "disabled"; + service_modules?: { + codex_agents?: boolean; + }; workspaces: Array<{ slug: string; name: string | null; diff --git a/plane-src/packages/constants/src/settings/workspace.ts b/plane-src/packages/constants/src/settings/workspace.ts index 08be1d2..03a4ebe 100644 --- a/plane-src/packages/constants/src/settings/workspace.ts +++ b/plane-src/packages/constants/src/settings/workspace.ts @@ -70,6 +70,13 @@ export const WORKSPACE_SETTINGS: Record pathname === `${baseUrl}/settings/ai-voice-tasker/`, }, + "codex-agent-api": { + key: "codex-agent-api", + i18n_label: "workspace_settings.settings.codex_agent_api.title", + href: `/settings/codex-agent-api`, + access: [EUserWorkspaceRoles.ADMIN], + highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/codex-agent-api/`, + }, }; export const WORKSPACE_SETTINGS_ACCESS = Object.fromEntries( @@ -84,6 +91,9 @@ export const GROUPED_WORKSPACE_SETTINGS: Record