FEAT - TASKER: показ Codex Agent API по Launcher entitlement
This commit is contained in:
parent
2ae353c8d5
commit
97566faba3
|
|
@ -25,6 +25,7 @@ def get_nodedc_workspace_creation_policy(user, workspace_slug=None):
|
||||||
"default_managed_by": "tasker",
|
"default_managed_by": "tasker",
|
||||||
"invite_approval": "tasker",
|
"invite_approval": "tasker",
|
||||||
"default_invite_approval": "tasker",
|
"default_invite_approval": "tasker",
|
||||||
|
"service_modules": {},
|
||||||
"workspaces": [],
|
"workspaces": [],
|
||||||
"reason": "NODE.DC workspace policy is not configured.",
|
"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",
|
"default_managed_by": "tasker",
|
||||||
"invite_approval": "tasker",
|
"invite_approval": "tasker",
|
||||||
"default_invite_approval": "tasker",
|
"default_invite_approval": "tasker",
|
||||||
|
"service_modules": {},
|
||||||
"workspaces": [],
|
"workspaces": [],
|
||||||
"reason": "NODE.DC identity is not linked." if enforce_unlinked else "Standalone user without NODE.DC identity.",
|
"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",
|
"default_managed_by": "tasker",
|
||||||
"invite_approval": "disabled",
|
"invite_approval": "disabled",
|
||||||
"default_invite_approval": "tasker",
|
"default_invite_approval": "tasker",
|
||||||
|
"service_modules": {},
|
||||||
"workspaces": [],
|
"workspaces": [],
|
||||||
"reason": "NODE.DC workspace policy is unavailable.",
|
"reason": "NODE.DC workspace policy is unavailable.",
|
||||||
}
|
}
|
||||||
|
|
||||||
workspace_policy = payload.get("workspacePolicy") if isinstance(payload.get("workspacePolicy"), dict) else {}
|
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"))
|
access_allowed = bool(payload.get("allowed"))
|
||||||
if not workspace_policy:
|
if not workspace_policy:
|
||||||
return {
|
return {
|
||||||
|
|
@ -90,6 +99,7 @@ def get_nodedc_workspace_creation_policy(user, workspace_slug=None):
|
||||||
"default_managed_by": "tasker",
|
"default_managed_by": "tasker",
|
||||||
"invite_approval": "tasker",
|
"invite_approval": "tasker",
|
||||||
"default_invite_approval": "tasker",
|
"default_invite_approval": "tasker",
|
||||||
|
"service_modules": service_modules,
|
||||||
"workspaces": [],
|
"workspaces": [],
|
||||||
"reason": payload.get("reason") or "NODE.DC access check does not expose workspace policy.",
|
"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")),
|
"default_managed_by": normalize_managed_by(workspace_policy.get("defaultManagedBy") or workspace_policy.get("managedBy")),
|
||||||
"invite_approval": invite_approval,
|
"invite_approval": invite_approval,
|
||||||
"default_invite_approval": default_invite_approval,
|
"default_invite_approval": default_invite_approval,
|
||||||
|
"service_modules": service_modules,
|
||||||
"workspaces": workspaces,
|
"workspaces": workspaces,
|
||||||
"reason": workspace_policy.get("reason") or payload.get("reason") or "NODE.DC workspace policy decision.",
|
"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
|
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):
|
def resolve_workspace_managed_by(workspace_slug, workspaces, fallback):
|
||||||
if isinstance(workspace_slug, str) and workspace_slug.strip():
|
if isinstance(workspace_slug, str) and workspace_slug.strip():
|
||||||
normalized_slug = workspace_slug.strip()
|
normalized_slug = workspace_slug.strip()
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -297,6 +297,10 @@ export const coreRoutes: RouteConfigEntry[] = [
|
||||||
":workspaceSlug/settings/ai-voice-tasker",
|
":workspaceSlug/settings/ai-voice-tasker",
|
||||||
"./(all)/[workspaceSlug]/(settings)/settings/(workspace)/ai-voice-tasker/page.tsx"
|
"./(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"
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
// --------------------------------------------------------------------
|
// --------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,12 @@ import { usePathname } from "next/navigation";
|
||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// plane imports
|
// 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 { useTranslation } from "@plane/i18n";
|
||||||
import type { TWorkspaceSettingsTabs } from "@plane/types";
|
import type { TWorkspaceSettingsTabs } from "@plane/types";
|
||||||
import { joinUrlPath } from "@plane/utils";
|
import { joinUrlPath } from "@plane/utils";
|
||||||
|
|
@ -19,12 +24,14 @@ import { SettingsSidebarItem } from "@/components/settings/sidebar/item";
|
||||||
import { useUserPermissions } from "@/hooks/store/user";
|
import { useUserPermissions } from "@/hooks/store/user";
|
||||||
// services
|
// services
|
||||||
import { WorkspaceAIService } from "@/services/workspace-ai.service";
|
import { WorkspaceAIService } from "@/services/workspace-ai.service";
|
||||||
|
import { WorkspaceService } from "@/services/workspace.service";
|
||||||
// local imports
|
// local imports
|
||||||
import { WORKSPACE_SETTINGS_ICONS } from "./item-icon";
|
import { WORKSPACE_SETTINGS_ICONS } from "./item-icon";
|
||||||
|
|
||||||
const HIDDEN_WORKSPACE_SETTINGS_KEYS = new Set(["billing-and-plans"]);
|
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 workspaceAIService = new WorkspaceAIService();
|
||||||
|
const workspaceService = new WorkspaceService();
|
||||||
|
|
||||||
export const WorkspaceSettingsSidebarItemCategories = observer(function WorkspaceSettingsSidebarItemCategories() {
|
export const WorkspaceSettingsSidebarItemCategories = observer(function WorkspaceSettingsSidebarItemCategories() {
|
||||||
// params
|
// params
|
||||||
|
|
@ -42,7 +49,12 @@ export const WorkspaceSettingsSidebarItemCategories = observer(function Workspac
|
||||||
canLoadVoiceTaskerEntitlement ? `WORKSPACE_AI_SETTINGS_${workspaceSlug}` : null,
|
canLoadVoiceTaskerEntitlement ? `WORKSPACE_AI_SETTINGS_${workspaceSlug}` : null,
|
||||||
() => workspaceAIService.retrieveSettings(workspaceSlug as string)
|
() => 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 isVoiceTaskerEntitled = aiSettings?.feature_entitlement_enabled === true;
|
||||||
|
const isCodexAgentEntitled = nodedcWorkspacePolicy?.service_modules?.codex_agents === true;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 flex flex-col divide-y divide-white/6">
|
<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(
|
const accessibleItems = categoryItems.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
!HIDDEN_WORKSPACE_SETTINGS_KEYS.has(item.key) &&
|
!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)
|
allowPermissions(item.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -59,7 +75,7 @@ export const WorkspaceSettingsSidebarItemCategories = observer(function Workspac
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={category} className="shrink-0 py-3.5 first:pt-0 last:pb-0">
|
<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)}
|
{t(category)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
|
|
@ -87,3 +103,16 @@ export const WorkspaceSettingsSidebarItemCategories = observer(function Workspac
|
||||||
</div>
|
</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 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
|
// plane imports
|
||||||
import type { ISvgIcons } from "@plane/propel/icons";
|
import type { ISvgIcons } from "@plane/propel/icons";
|
||||||
import type { TWorkspaceSettingsTabs } from "@plane/types";
|
import type { TWorkspaceSettingsTabs } from "@plane/types";
|
||||||
|
|
@ -18,4 +18,5 @@ export const WORKSPACE_SETTINGS_ICONS: Record<TWorkspaceSettingsTabs, LucideIcon
|
||||||
storage: Database,
|
storage: Database,
|
||||||
webhooks: Webhook,
|
webhooks: Webhook,
|
||||||
"ai-voice-tasker": Mic,
|
"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 { WORKSPACE_SETTINGS_ICONS } from "@/components/settings/workspace/sidebar/item-icon";
|
||||||
import { WorkspaceSettingsSidebarHeader } from "@/components/settings/workspace/sidebar/header";
|
import { WorkspaceSettingsSidebarHeader } from "@/components/settings/workspace/sidebar/header";
|
||||||
import { AIVoiceTaskerSettingsContent } from "@/components/workspace/settings/ai-voice-tasker-settings";
|
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 { WorkspaceExportsSettingsContent } from "@/components/workspace/settings/exports-settings";
|
||||||
import { WorkspaceMembersSettingsContent } from "@/components/workspace/settings/members-settings";
|
import { WorkspaceMembersSettingsContent } from "@/components/workspace/settings/members-settings";
|
||||||
import { StorageSettingsContent } from "@/components/workspace/settings/storage-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 HIDDEN_WORKSPACE_SETTINGS_KEYS = new Set<TWorkspaceSettingsTabs>(["billing-and-plans"]);
|
||||||
const LAUNCHER_MANAGED_HIDDEN_WORKSPACE_SETTINGS_KEYS = new Set<TWorkspaceSettingsTabs>(["members"]);
|
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>([
|
const MODAL_TABS = new Set<TWorkspaceSettingsTabs>([
|
||||||
"general",
|
"general",
|
||||||
"members",
|
"members",
|
||||||
|
|
@ -56,6 +57,7 @@ const MODAL_TABS = new Set<TWorkspaceSettingsTabs>([
|
||||||
"storage",
|
"storage",
|
||||||
"webhooks",
|
"webhooks",
|
||||||
"ai-voice-tasker",
|
"ai-voice-tasker",
|
||||||
|
"codex-agent-api",
|
||||||
]);
|
]);
|
||||||
const workspaceAIService = new WorkspaceAIService();
|
const workspaceAIService = new WorkspaceAIService();
|
||||||
const workspaceService = new WorkspaceService();
|
const workspaceService = new WorkspaceService();
|
||||||
|
|
@ -99,6 +101,7 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal()
|
||||||
);
|
);
|
||||||
const isVoiceTaskerEntitled = aiSettings?.feature_entitlement_enabled === true;
|
const isVoiceTaskerEntitled = aiSettings?.feature_entitlement_enabled === true;
|
||||||
const isLauncherManagedWorkspace = nodedcWorkspacePolicy?.managed_by === "launcher";
|
const isLauncherManagedWorkspace = nodedcWorkspacePolicy?.managed_by === "launcher";
|
||||||
|
const isCodexAgentEntitled = nodedcWorkspacePolicy?.service_modules?.codex_agents === true;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const syncFromLocation = () => {
|
const syncFromLocation = () => {
|
||||||
|
|
@ -136,6 +139,11 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal()
|
||||||
if (!isVoiceTaskerEntitled) openWorkspaceSettingsModal("general", true);
|
if (!isVoiceTaskerEntitled) openWorkspaceSettingsModal("general", true);
|
||||||
}, [activeTab, isOpen, isVoiceTaskerEntitlementLoading, isVoiceTaskerEntitled]);
|
}, [activeTab, isOpen, isVoiceTaskerEntitlementLoading, isVoiceTaskerEntitled]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen || activeTab !== "codex-agent-api" || !nodedcWorkspacePolicy) return;
|
||||||
|
if (!isCodexAgentEntitled) openWorkspaceSettingsModal("general", true);
|
||||||
|
}, [activeTab, isCodexAgentEntitled, isOpen, nodedcWorkspacePolicy]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen || activeTab !== "members" || !isLauncherManagedWorkspace) return;
|
if (!isOpen || activeTab !== "members" || !isLauncherManagedWorkspace) return;
|
||||||
openWorkspaceSettingsModal("general", true);
|
openWorkspaceSettingsModal("general", true);
|
||||||
|
|
@ -162,6 +170,11 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal()
|
||||||
return <AIVoiceTaskerSettingsContent workspaceSlug={currentWorkspace.slug} />;
|
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) {
|
if (activeTab === "members" && currentWorkspace?.slug) {
|
||||||
return <WorkspaceMembersSettingsContent workspaceSlug={currentWorkspace.slug} />;
|
return <WorkspaceMembersSettingsContent workspaceSlug={currentWorkspace.slug} />;
|
||||||
}
|
}
|
||||||
|
|
@ -204,6 +217,7 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal()
|
||||||
allowPermissions={allowPermissions}
|
allowPermissions={allowPermissions}
|
||||||
isVoiceTaskerEntitled={isVoiceTaskerEntitled}
|
isVoiceTaskerEntitled={isVoiceTaskerEntitled}
|
||||||
isLauncherManagedWorkspace={isLauncherManagedWorkspace}
|
isLauncherManagedWorkspace={isLauncherManagedWorkspace}
|
||||||
|
isCodexAgentEntitled={isCodexAgentEntitled}
|
||||||
workspaceSlug={currentWorkspace?.slug}
|
workspaceSlug={currentWorkspace?.slug}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -238,6 +252,7 @@ export const WorkspaceSettingsModal = observer(function WorkspaceSettingsModal()
|
||||||
type TWorkspaceModalSidebarProps = {
|
type TWorkspaceModalSidebarProps = {
|
||||||
activeTab: TWorkspaceSettingsModalTab;
|
activeTab: TWorkspaceSettingsModalTab;
|
||||||
allowPermissions: ReturnType<typeof useUserPermissions>["allowPermissions"];
|
allowPermissions: ReturnType<typeof useUserPermissions>["allowPermissions"];
|
||||||
|
isCodexAgentEntitled: boolean;
|
||||||
isLauncherManagedWorkspace: boolean;
|
isLauncherManagedWorkspace: boolean;
|
||||||
isVoiceTaskerEntitled: boolean;
|
isVoiceTaskerEntitled: boolean;
|
||||||
onSelectItem: (itemKey: TWorkspaceSettingsTabs, itemHref: string) => void;
|
onSelectItem: (itemKey: TWorkspaceSettingsTabs, itemHref: string) => void;
|
||||||
|
|
@ -247,6 +262,7 @@ type TWorkspaceModalSidebarProps = {
|
||||||
function WorkspaceModalSidebar({
|
function WorkspaceModalSidebar({
|
||||||
activeTab,
|
activeTab,
|
||||||
allowPermissions,
|
allowPermissions,
|
||||||
|
isCodexAgentEntitled,
|
||||||
isLauncherManagedWorkspace,
|
isLauncherManagedWorkspace,
|
||||||
isVoiceTaskerEntitled,
|
isVoiceTaskerEntitled,
|
||||||
onSelectItem,
|
onSelectItem,
|
||||||
|
|
@ -267,7 +283,11 @@ function WorkspaceModalSidebar({
|
||||||
(item) =>
|
(item) =>
|
||||||
!HIDDEN_WORKSPACE_SETTINGS_KEYS.has(item.key) &&
|
!HIDDEN_WORKSPACE_SETTINGS_KEYS.has(item.key) &&
|
||||||
(!isLauncherManagedWorkspace || !LAUNCHER_MANAGED_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)
|
allowPermissions(item.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -302,3 +322,16 @@ function WorkspaceModalSidebar({
|
||||||
</ScrollArea>
|
</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 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 = {
|
type TWorkspaceSettingsModalEventDetail = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|
@ -23,7 +30,8 @@ export const getWorkspaceSettingsModalTabFromSearch = (search: string): TWorkspa
|
||||||
value === "export" ||
|
value === "export" ||
|
||||||
value === "storage" ||
|
value === "storage" ||
|
||||||
value === "webhooks" ||
|
value === "webhooks" ||
|
||||||
value === "ai-voice-tasker"
|
value === "ai-voice-tasker" ||
|
||||||
|
value === "codex-agent-api"
|
||||||
)
|
)
|
||||||
return value;
|
return value;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,9 @@ export interface NodeDCWorkspacePolicy {
|
||||||
default_managed_by: "launcher" | "tasker";
|
default_managed_by: "launcher" | "tasker";
|
||||||
invite_approval: "tasker" | "nodedc" | "launcher" | "disabled";
|
invite_approval: "tasker" | "nodedc" | "launcher" | "disabled";
|
||||||
default_invite_approval: "tasker" | "nodedc" | "launcher" | "disabled";
|
default_invite_approval: "tasker" | "nodedc" | "launcher" | "disabled";
|
||||||
|
service_modules?: {
|
||||||
|
codex_agents?: boolean;
|
||||||
|
};
|
||||||
workspaces: Array<{
|
workspaces: Array<{
|
||||||
slug: string;
|
slug: string;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,13 @@ export const WORKSPACE_SETTINGS: Record<TWorkspaceSettingsTabs, TWorkspaceSettin
|
||||||
access: [EUserWorkspaceRoles.ADMIN],
|
access: [EUserWorkspaceRoles.ADMIN],
|
||||||
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/ai-voice-tasker/`,
|
highlight: (pathname: string, baseUrl: string) => 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(
|
export const WORKSPACE_SETTINGS_ACCESS = Object.fromEntries(
|
||||||
|
|
@ -84,6 +91,9 @@ export const GROUPED_WORKSPACE_SETTINGS: Record<WORKSPACE_SETTINGS_CATEGORY, TWo
|
||||||
WORKSPACE_SETTINGS["export"],
|
WORKSPACE_SETTINGS["export"],
|
||||||
WORKSPACE_SETTINGS["storage"],
|
WORKSPACE_SETTINGS["storage"],
|
||||||
],
|
],
|
||||||
[WORKSPACE_SETTINGS_CATEGORY.FEATURES]: [WORKSPACE_SETTINGS["ai-voice-tasker"]],
|
[WORKSPACE_SETTINGS_CATEGORY.FEATURES]: [
|
||||||
|
WORKSPACE_SETTINGS["ai-voice-tasker"],
|
||||||
|
WORKSPACE_SETTINGS["codex-agent-api"],
|
||||||
|
],
|
||||||
[WORKSPACE_SETTINGS_CATEGORY.DEVELOPER]: [WORKSPACE_SETTINGS["webhooks"]],
|
[WORKSPACE_SETTINGS_CATEGORY.DEVELOPER]: [WORKSPACE_SETTINGS["webhooks"]],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -424,7 +424,8 @@ export default {
|
||||||
},
|
},
|
||||||
decline_modal: {
|
decline_modal: {
|
||||||
title: "Return the request for rework",
|
title: "Return the request for rework",
|
||||||
description: "Provide the reason for returning the request. This comment will be sent to the external contour and added to the target issue.",
|
description:
|
||||||
|
"Provide the reason for returning the request. This comment will be sent to the external contour and added to the target issue.",
|
||||||
placeholder: "Describe what needs to be revised or clarified",
|
placeholder: "Describe what needs to be revised or clarified",
|
||||||
submit: "Decline and return",
|
submit: "Decline and return",
|
||||||
},
|
},
|
||||||
|
|
@ -1794,6 +1795,9 @@ export default {
|
||||||
ai_voice_tasker: {
|
ai_voice_tasker: {
|
||||||
title: "AI / Voice Tasker",
|
title: "AI / Voice Tasker",
|
||||||
},
|
},
|
||||||
|
codex_agent_api: {
|
||||||
|
title: "Codex Agent API",
|
||||||
|
},
|
||||||
api_tokens: {
|
api_tokens: {
|
||||||
title: "Personal Access Tokens",
|
title: "Personal Access Tokens",
|
||||||
add_token: "Add personal access token",
|
add_token: "Add personal access token",
|
||||||
|
|
@ -1991,7 +1995,8 @@ export default {
|
||||||
},
|
},
|
||||||
list_heading: "Estimate list",
|
list_heading: "Estimate list",
|
||||||
archived_heading: "Archived estimates",
|
archived_heading: "Archived estimates",
|
||||||
archived_description: "These are estimates from earlier project versions that are not currently in use. Read more",
|
archived_description:
|
||||||
|
"These are estimates from earlier project versions that are not currently in use. Read more",
|
||||||
no_estimate: "No estimate",
|
no_estimate: "No estimate",
|
||||||
new: "New estimate system",
|
new: "New estimate system",
|
||||||
create: {
|
create: {
|
||||||
|
|
@ -2395,7 +2400,8 @@ export default {
|
||||||
project_page: {
|
project_page: {
|
||||||
delete_modal: {
|
delete_modal: {
|
||||||
title: "Delete page",
|
title: "Delete page",
|
||||||
content: 'Are you sure you want to delete page "{value}"? The page will be permanently removed and this action cannot be undone.',
|
content:
|
||||||
|
'Are you sure you want to delete page "{value}"? The page will be permanently removed and this action cannot be undone.',
|
||||||
success_title: "Page deleted",
|
success_title: "Page deleted",
|
||||||
success_message: "Page deleted successfully.",
|
success_message: "Page deleted successfully.",
|
||||||
error_title: "Page delete failed",
|
error_title: "Page delete failed",
|
||||||
|
|
@ -3108,24 +3114,24 @@ export default {
|
||||||
project_settings_label: "Project settings",
|
project_settings_label: "Project settings",
|
||||||
project_join_modal: {
|
project_join_modal: {
|
||||||
title: "Join project?",
|
title: "Join project?",
|
||||||
description: "Are you sure you want to join the project {project}? Click \"Join project\" to continue.",
|
description: 'Are you sure you want to join the project {project}? Click "Join project" to continue.',
|
||||||
submit: "Join project",
|
submit: "Join project",
|
||||||
loading: "Joining...",
|
loading: "Joining...",
|
||||||
},
|
},
|
||||||
project_leave_modal: {
|
project_leave_modal: {
|
||||||
title: "Leave project",
|
title: "Leave project",
|
||||||
description:
|
description:
|
||||||
"Are you sure you want to leave the project \"{project}\"? All work items associated with you will become inaccessible.",
|
'Are you sure you want to leave the project "{project}"? All work items associated with you will become inaccessible.',
|
||||||
enter_project_name: "Enter the project name {project} to continue:",
|
enter_project_name: "Enter the project name {project} to continue:",
|
||||||
project_name_placeholder: "Enter project name",
|
project_name_placeholder: "Enter project name",
|
||||||
confirm_instruction: "To confirm, type {keyword} below:",
|
confirm_instruction: "To confirm, type {keyword} below:",
|
||||||
confirm_placeholder: "Enter \"leave project\"",
|
confirm_placeholder: 'Enter "leave project"',
|
||||||
confirm_keyword: "leave project",
|
confirm_keyword: "leave project",
|
||||||
loading: "Leaving...",
|
loading: "Leaving...",
|
||||||
submit: "Leave project",
|
submit: "Leave project",
|
||||||
error_title: "Error!",
|
error_title: "Error!",
|
||||||
error_default: "Something went wrong. Please try again later.",
|
error_default: "Something went wrong. Please try again later.",
|
||||||
error_confirm: "Please confirm leaving the project by typing \"leave project\".",
|
error_confirm: 'Please confirm leaving the project by typing "leave project".',
|
||||||
error_name: "Please enter the project name exactly as shown in the description.",
|
error_name: "Please enter the project name exactly as shown in the description.",
|
||||||
error_fields: "Please fill all fields.",
|
error_fields: "Please fill all fields.",
|
||||||
},
|
},
|
||||||
|
|
@ -3136,7 +3142,7 @@ export default {
|
||||||
enter_project_name: "Enter the project name {project} to continue:",
|
enter_project_name: "Enter the project name {project} to continue:",
|
||||||
project_name_placeholder: "Project name",
|
project_name_placeholder: "Project name",
|
||||||
confirm_instruction: "To confirm, type {keyword} below:",
|
confirm_instruction: "To confirm, type {keyword} below:",
|
||||||
confirm_placeholder: "Enter \"delete my project\"",
|
confirm_placeholder: 'Enter "delete my project"',
|
||||||
confirm_keyword: "delete my project",
|
confirm_keyword: "delete my project",
|
||||||
loading: "Deleting",
|
loading: "Deleting",
|
||||||
submit: "Delete project",
|
submit: "Delete project",
|
||||||
|
|
@ -3151,7 +3157,7 @@ export default {
|
||||||
type_workspace_name: "Type this workspace name to continue.",
|
type_workspace_name: "Type this workspace name to continue.",
|
||||||
final_confirmation: "For final confirmation, type {keyword} below.",
|
final_confirmation: "For final confirmation, type {keyword} below.",
|
||||||
confirm_keyword: "delete my workspace",
|
confirm_keyword: "delete my workspace",
|
||||||
input_placeholder: "Enter \"delete my workspace\"",
|
input_placeholder: 'Enter "delete my workspace"',
|
||||||
},
|
},
|
||||||
project_invitation_modal: {
|
project_invitation_modal: {
|
||||||
success_title: "Success!",
|
success_title: "Success!",
|
||||||
|
|
|
||||||
|
|
@ -552,7 +552,8 @@ export default {
|
||||||
},
|
},
|
||||||
traceability: {
|
traceability: {
|
||||||
title: "Маршрутизация",
|
title: "Маршрутизация",
|
||||||
description: "Здесь отображается, из какого контура ушёл запрос, куда он направлен и в каком состоянии находится работа по нему.",
|
description:
|
||||||
|
"Здесь отображается, из какого контура ушёл запрос, куда он направлен и в каком состоянии находится работа по нему.",
|
||||||
source_contour: "Исходный внутренний контур",
|
source_contour: "Исходный внутренний контур",
|
||||||
source_decision: "Решение источника",
|
source_decision: "Решение источника",
|
||||||
source_decision_pending: "Ожидает решения",
|
source_decision_pending: "Ожидает решения",
|
||||||
|
|
@ -1956,6 +1957,9 @@ export default {
|
||||||
ai_voice_tasker: {
|
ai_voice_tasker: {
|
||||||
title: "AI / Voice Tasker",
|
title: "AI / Voice Tasker",
|
||||||
},
|
},
|
||||||
|
codex_agent_api: {
|
||||||
|
title: "Codex Agent API",
|
||||||
|
},
|
||||||
api_tokens: {
|
api_tokens: {
|
||||||
title: "API-токены",
|
title: "API-токены",
|
||||||
add_token: "Добавить токен",
|
add_token: "Добавить токен",
|
||||||
|
|
@ -2552,7 +2556,8 @@ export default {
|
||||||
project_page: {
|
project_page: {
|
||||||
delete_modal: {
|
delete_modal: {
|
||||||
title: "Удалить страницу",
|
title: "Удалить страницу",
|
||||||
content: 'Вы уверены, что хотите удалить страницу "{value}"? Страница будет удалена без возможности восстановления.',
|
content:
|
||||||
|
'Вы уверены, что хотите удалить страницу "{value}"? Страница будет удалена без возможности восстановления.',
|
||||||
success_title: "Страница удалена",
|
success_title: "Страница удалена",
|
||||||
success_message: "Страница успешно удалена.",
|
success_message: "Страница успешно удалена.",
|
||||||
error_title: "Не удалось удалить страницу",
|
error_title: "Не удалось удалить страницу",
|
||||||
|
|
@ -3379,8 +3384,7 @@ export default {
|
||||||
},
|
},
|
||||||
cycles: {
|
cycles: {
|
||||||
title: "Двигайтесь циклами",
|
title: "Двигайтесь циклами",
|
||||||
description:
|
description: "Циклы помогают команде двигаться быстрее и ближе всего соответствуют спринтам в agile-подходе.",
|
||||||
"Циклы помогают команде двигаться быстрее и ближе всего соответствуют спринтам в agile-подходе.",
|
|
||||||
},
|
},
|
||||||
modules: {
|
modules: {
|
||||||
title: "Делите работу на модули",
|
title: "Делите работу на модули",
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ export type TWorkspaceSettingsTabs =
|
||||||
| "export"
|
| "export"
|
||||||
| "storage"
|
| "storage"
|
||||||
| "webhooks"
|
| "webhooks"
|
||||||
| "ai-voice-tasker";
|
| "ai-voice-tasker"
|
||||||
|
| "codex-agent-api";
|
||||||
export type TWorkspaceSettingsItem = {
|
export type TWorkspaceSettingsItem = {
|
||||||
key: TWorkspaceSettingsTabs;
|
key: TWorkspaceSettingsTabs;
|
||||||
i18n_label: string;
|
i18n_label: string;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue