FEAT - TASKER: показ Codex Agent API по Launcher entitlement

This commit is contained in:
DCCONSTRUCTIONS
2026-05-14 20:49:52 +03:00
parent 2ae353c8d5
commit 97566faba3
13 changed files with 276 additions and 34 deletions
@@ -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<TWorkspaceSettingsTabs>(["ai-voice-tasker"]);
const WORKSPACE_FEATURE_GATED_SETTINGS_KEYS = new Set<TWorkspaceSettingsTabs>(["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 (
<div className="mt-4 flex flex-col divide-y divide-white/6">
@@ -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 (
<div key={category} className="shrink-0 py-3.5 first:pt-0 last:pb-0">
<div className="px-3 py-1.5 text-[11px] font-semibold uppercase tracking-[0.18em] text-tertiary">
<div className="px-3 py-1.5 text-[11px] font-semibold tracking-[0.18em] text-tertiary uppercase">
{t(category)}
</div>
<div className="flex flex-col">
@@ -87,3 +103,16 @@ export const WorkspaceSettingsSidebarItemCategories = observer(function Workspac
</div>
);
});
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;
}
@@ -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<TWorkspaceSettingsTabs, LucideIcon
storage: Database,
webhooks: Webhook,
"ai-voice-tasker": Mic,
"codex-agent-api": Bot,
};
@@ -0,0 +1,108 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
import { Bot, Check, KeyRound, Route, ShieldCheck } from "lucide-react";
import useSWR from "swr";
// components
import { SettingsHeading } from "@/components/settings/heading";
// hooks
import { useWorkspace } from "@/hooks/store/use-workspace";
// services
import { WorkspaceService } from "@/services/workspace.service";
const workspaceService = new WorkspaceService();
type TProps = {
showHeading?: boolean;
workspaceSlug: string;
};
export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSettingsContent(props: TProps) {
const { showHeading = true, workspaceSlug } = props;
const { currentWorkspace } = useWorkspace();
const { data: nodedcWorkspacePolicy, isLoading } = useSWR(
workspaceSlug ? `NODEDC_WORKSPACE_POLICY_${workspaceSlug}` : null,
() => workspaceService.getNodeDCWorkspacePolicy(workspaceSlug)
);
const isCodexAgentEntitled = nodedcWorkspacePolicy?.service_modules?.codex_agents === true;
return (
<div className="flex w-full flex-col gap-7">
{showHeading && (
<SettingsHeading
title="Codex Agent API"
description="Workspace-level вход в отдельный NODE.DC Agent Gateway. Внешний Codex получает только ограниченные agent grants, а не Plane session cookies или прямой Tasker API."
/>
)}
{isLoading ? (
<div className="nodedc-settings-card text-sm px-5 py-5 text-secondary">Загрузка статуса модуля...</div>
) : (
<>
<section className="nodedc-settings-card overflow-hidden">
<div className="flex flex-col gap-4 px-5 py-5 md:flex-row md:items-start md:justify-between">
<div className="min-w-0">
<div className="flex items-center gap-2 text-16 font-semibold text-primary">
<Bot className="size-5 text-tertiary" />
<span>Agent Gateway для {currentWorkspace?.name ?? workspaceSlug}</span>
</div>
<p className="mt-2 max-w-3xl text-13 leading-5 text-secondary">
Доступ к модулю приходит из Launcher entitlement Operational Core Codex Agent API. Если entitlement
снят, этот раздел исчезает из настроек workspace и backend policy больше не возвращает активный модуль.
</p>
</div>
<div className="nodedc-external-readonly-value shrink-0">
<span className="grid size-5 place-items-center rounded-full bg-[rgb(var(--nodedc-accent-rgb))] text-[rgb(var(--nodedc-on-accent-rgb))]">
<Check className="size-3.5" />
</span>
<span>{isCodexAgentEntitled ? "Доступ выдан" : "Доступ не выдан"}</span>
</div>
</div>
</section>
<section className="grid gap-4 md:grid-cols-3">
<CapabilityCard
icon={ShieldCheck}
title="Граница прав"
description="Агент работает только в workspace/project grants и не получает права на удаление карточек, проектов, участников или состояний."
/>
<CapabilityCard
icon={Route}
title="Маршрутизация"
description="Все write-действия идут через отдельный Gateway и Tasker internal adapter, с audit trail и idempotency key."
/>
<CapabilityCard
icon={KeyRound}
title="Локальный Codex"
description="Пользовательский Codex подключается по MCP endpoint с agent token; token хранится только на стороне Gateway."
/>
</section>
</>
)}
</div>
);
});
type TCapabilityCardProps = {
description: string;
icon: typeof ShieldCheck;
title: string;
};
function CapabilityCard(props: TCapabilityCardProps) {
const Icon = props.icon;
return (
<div className="nodedc-settings-card px-5 py-5">
<div className="flex items-center gap-2 text-14 font-semibold text-primary">
<Icon className="size-4 text-tertiary" />
<span>{props.title}</span>
</div>
<p className="mt-3 text-13 leading-5 text-secondary">{props.description}</p>
</div>
);
}
@@ -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<TWorkspaceSettingsTabs>(["billing-and-plans"]);
const LAUNCHER_MANAGED_HIDDEN_WORKSPACE_SETTINGS_KEYS = new Set<TWorkspaceSettingsTabs>(["members"]);
const WORKSPACE_FEATURE_GATED_SETTINGS_KEYS = new Set<TWorkspaceSettingsTabs>(["ai-voice-tasker"]);
const WORKSPACE_FEATURE_GATED_SETTINGS_KEYS = new Set<TWorkspaceSettingsTabs>(["ai-voice-tasker", "codex-agent-api"]);
const MODAL_TABS = new Set<TWorkspaceSettingsTabs>([
"general",
"members",
@@ -56,6 +57,7 @@ const MODAL_TABS = new Set<TWorkspaceSettingsTabs>([
"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 <AIVoiceTaskerSettingsContent workspaceSlug={currentWorkspace.slug} />;
}
if (activeTab === "codex-agent-api" && currentWorkspace?.slug) {
if (!isCodexAgentEntitled) return <WorkspaceDetails />;
return <CodexAgentApiSettingsContent workspaceSlug={currentWorkspace.slug} />;
}
if (activeTab === "members" && currentWorkspace?.slug) {
return <WorkspaceMembersSettingsContent workspaceSlug={currentWorkspace.slug} />;
}
@@ -204,6 +217,7 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal()
allowPermissions={allowPermissions}
isVoiceTaskerEntitled={isVoiceTaskerEntitled}
isLauncherManagedWorkspace={isLauncherManagedWorkspace}
isCodexAgentEntitled={isCodexAgentEntitled}
workspaceSlug={currentWorkspace?.slug}
/>
</div>
@@ -238,6 +252,7 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal()
type TWorkspaceModalSidebarProps = {
activeTab: TWorkspaceSettingsModalTab;
allowPermissions: ReturnType<typeof useUserPermissions>["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({
</ScrollArea>
);
}
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;
}
@@ -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;