From f20209ecc5d58e3a1dcdf05cfce2c7f9d690a373 Mon Sep 17 00:00:00 2001 From: Atef Bellaaj Date: Tue, 1 Jul 2025 17:46:07 +0200 Subject: [PATCH] refactor: Replace marketplace interface config with permission-based system - Add MARKETPLACE permission type to handle marketplace access control - Update interface configuration to use role-based marketplace settings (admin/user) - Replace direct marketplace boolean config with permission-based checks - Modify frontend components to use marketplace permissions instead of interface config - Update agent query hooks to use marketplace permissions for determining permission levels - Add marketplace configuration structure similar to peoplePicker in YAML config - Backend now sets MARKETPLACE permissions based on interface configuration - When marketplace enabled: users get agents with EDIT permissions in dropdown lists (builder mode) - When marketplace disabled: users get agents with VIEW permissions in dropdown lists (browse mode) --- api/server/services/start/interface.js | 14 +++++++++++ client/src/components/Nav/NewChat.tsx | 9 ++++--- .../SidePanel/Agents/AgentMarketplace.tsx | 11 +++++++- .../SidePanel/Agents/AgentSelect.tsx | 16 ++++++------ client/src/data-provider/Agents/queries.ts | 25 ++++++++++++++++++- client/src/hooks/Agents/useAgentsMap.ts | 8 +++--- librechat.example.yaml | 12 +++++++++ packages/data-provider/src/config.ts | 22 ++++++++++++++++ packages/data-provider/src/permissions.ts | 10 ++++++++ packages/data-provider/src/roles.ts | 9 +++++++ packages/data-schemas/src/schema/role.ts | 4 +++ packages/data-schemas/src/types/role.ts | 3 +++ 12 files changed, 128 insertions(+), 15 deletions(-) diff --git a/api/server/services/start/interface.js b/api/server/services/start/interface.js index 6f9b6ef20..82bd63a04 100644 --- a/api/server/services/start/interface.js +++ b/api/server/services/start/interface.js @@ -62,6 +62,14 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol groups: interfaceConfig?.peoplePicker?.user?.groups ?? defaults.peoplePicker.user.groups, }, }, + marketplace: { + admin: { + use: interfaceConfig?.marketplace?.admin?.use ?? defaults.marketplace.admin.use, + }, + user: { + use: interfaceConfig?.marketplace?.user?.use ?? defaults.marketplace.user.use, + }, + }, }); await updateAccessPermissions(roleName, { @@ -80,6 +88,9 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol [Permissions.VIEW_USERS]: loadedInterface.peoplePicker.user?.users, [Permissions.VIEW_GROUPS]: loadedInterface.peoplePicker.user?.groups, }, + [PermissionTypes.MARKETPLACE]: { + [Permissions.USE]: loadedInterface.marketplace.user?.use, + }, [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: loadedInterface.fileSearch }, }); await updateAccessPermissions(SystemRoles.ADMIN, { @@ -98,6 +109,9 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol [Permissions.VIEW_USERS]: loadedInterface.peoplePicker.admin?.users, [Permissions.VIEW_GROUPS]: loadedInterface.peoplePicker.admin?.groups, }, + [PermissionTypes.MARKETPLACE]: { + [Permissions.USE]: loadedInterface.marketplace.admin?.use, + }, [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: loadedInterface.fileSearch }, }); diff --git a/client/src/components/Nav/NewChat.tsx b/client/src/components/Nav/NewChat.tsx index 4348a3bad..94fb8f577 100644 --- a/client/src/components/Nav/NewChat.tsx +++ b/client/src/components/Nav/NewChat.tsx @@ -34,6 +34,10 @@ export default function NewChat({ permissionType: PermissionTypes.AGENTS, permission: Permissions.USE, }); + const hasAccessToMarketplace = useHasAccess({ + permissionType: PermissionTypes.MARKETPLACE, + permission: Permissions.USE, + }); const clickHandler: React.MouseEventHandler = useCallback( (e) => { @@ -67,9 +71,8 @@ export default function NewChat({ authContext?.isAuthenticated !== undefined && (authContext?.isAuthenticated === false || authContext?.user !== undefined); - // Show agent marketplace when auth is ready and user has access - // Note: endpointsConfig[agents] is null, but we can still show the marketplace - const showAgentMarketplace = authReady && hasAccessToAgents; + // Show agent marketplace when marketplace permission is enabled, auth is ready, and user has access to agents + const showAgentMarketplace = authReady && hasAccessToAgents && hasAccessToMarketplace; return ( <> diff --git a/client/src/components/SidePanel/Agents/AgentMarketplace.tsx b/client/src/components/SidePanel/Agents/AgentMarketplace.tsx index 78740c7ed..3557a0119 100644 --- a/client/src/components/SidePanel/Agents/AgentMarketplace.tsx +++ b/client/src/components/SidePanel/Agents/AgentMarketplace.tsx @@ -7,7 +7,7 @@ import type t from 'librechat-data-provider'; import type { ContextType } from '~/common'; import { useGetEndpointsQuery, useGetAgentCategoriesQuery } from '~/data-provider'; -import { useDocumentTitle } from '~/hooks'; +import { useDocumentTitle, useHasAccess } from '~/hooks'; import useLocalize from '~/hooks/useLocalize'; import { TooltipAnchor, Button } from '~/components/ui'; import { NewChatIcon } from '~/components/svg'; @@ -19,6 +19,7 @@ import AgentDetail from './AgentDetail'; import SearchBar from './SearchBar'; import AgentGrid from './AgentGrid'; import store from '~/store'; +import { PermissionTypes, Permissions } from 'librechat-data-provider'; interface AgentMarketplaceProps { className?: string; @@ -168,6 +169,14 @@ const AgentMarketplace: React.FC = ({ className = '' }) = const fullCollapse = useMemo(() => localStorage.getItem('fullPanelCollapse') === 'true', []); + const hasAccessToMarketplace = useHasAccess({ + permissionType: PermissionTypes.MARKETPLACE, + permission: Permissions.USE, + }); + if (!hasAccessToMarketplace) { + navigate('/not-found', { replace: true }); + return null; + } return (
diff --git a/client/src/components/SidePanel/Agents/AgentSelect.tsx b/client/src/components/SidePanel/Agents/AgentSelect.tsx index bd8ce5860..3f2da4a0c 100644 --- a/client/src/components/SidePanel/Agents/AgentSelect.tsx +++ b/client/src/components/SidePanel/Agents/AgentSelect.tsx @@ -1,16 +1,16 @@ import { EarthIcon } from 'lucide-react'; import { useCallback, useEffect, useRef } from 'react'; import { useFormContext, Controller } from 'react-hook-form'; -import { - AgentCapabilities, - defaultAgentFormValues, - PERMISSION_BITS, -} from 'librechat-data-provider'; +import { AgentCapabilities, defaultAgentFormValues } from 'librechat-data-provider'; import type { UseMutationResult, QueryObserverResult } from '@tanstack/react-query'; import type { Agent, AgentCreateParams } from 'librechat-data-provider'; import type { TAgentCapabilities, AgentForm } from '~/common'; import { cn, createProviderOption, processAgentOption, getDefaultAgentFormValues } from '~/utils'; -import { useListAgentsQuery, useGetStartupConfig } from '~/data-provider'; +import { + useListAgentsQuery, + useGetStartupConfig, + useAgentListingDefaultPermissionLevel, +} from '~/data-provider'; import ControlCombobox from '~/components/ui/ControlCombobox'; import { useLocalize } from '~/hooks'; @@ -32,8 +32,10 @@ export default function AgentSelect({ const { control, reset } = useFormContext(); const { data: startupConfig } = useGetStartupConfig(); + const permissionLevel = useAgentListingDefaultPermissionLevel(); + const { data: agents = null } = useListAgentsQuery( - { requiredPermission: PERMISSION_BITS.EDIT }, + { requiredPermission: permissionLevel }, { select: (res) => res.data.map((agent) => diff --git a/client/src/data-provider/Agents/queries.ts b/client/src/data-provider/Agents/queries.ts index 0fb35beeb..5f253f6c8 100644 --- a/client/src/data-provider/Agents/queries.ts +++ b/client/src/data-provider/Agents/queries.ts @@ -1,11 +1,34 @@ -import { QueryKeys, dataService, EModelEndpoint, PERMISSION_BITS } from 'librechat-data-provider'; +import { + QueryKeys, + dataService, + EModelEndpoint, + PERMISSION_BITS, + PermissionTypes, + Permissions, +} from 'librechat-data-provider'; import { useQuery, useInfiniteQuery, useQueryClient } from '@tanstack/react-query'; +import { useMemo } from 'react'; import type { QueryObserverResult, UseQueryOptions, UseInfiniteQueryOptions, } from '@tanstack/react-query'; import type t from 'librechat-data-provider'; +import { useHasAccess } from '~/hooks'; + +/** + * Hook to determine the appropriate permission level for agent queries based on marketplace configuration + */ +export const useAgentListingDefaultPermissionLevel = () => { + const hasMarketplaceAccess = useHasAccess({ + permissionType: PermissionTypes.MARKETPLACE, + permission: Permissions.USE, + }); + + // When marketplace is active: EDIT permissions (builder mode) + // When marketplace is not active: VIEW permissions (browse mode) + return hasMarketplaceAccess ? PERMISSION_BITS.EDIT : PERMISSION_BITS.VIEW; +}; /** * AGENTS diff --git a/client/src/hooks/Agents/useAgentsMap.ts b/client/src/hooks/Agents/useAgentsMap.ts index d872e6f34..112f75a25 100644 --- a/client/src/hooks/Agents/useAgentsMap.ts +++ b/client/src/hooks/Agents/useAgentsMap.ts @@ -1,6 +1,6 @@ -import { PERMISSION_BITS, TAgentsMap } from 'librechat-data-provider'; +import { TAgentsMap } from 'librechat-data-provider'; import { useMemo } from 'react'; -import { useListAgentsQuery } from '~/data-provider'; +import { useListAgentsQuery, useAgentListingDefaultPermissionLevel } from '~/data-provider'; import { mapAgents } from '~/utils'; export default function useAgentsMap({ @@ -8,8 +8,10 @@ export default function useAgentsMap({ }: { isAuthenticated: boolean; }): TAgentsMap | undefined { + const permissionLevel = useAgentListingDefaultPermissionLevel(); + const { data: agentsList = null } = useListAgentsQuery( - { requiredPermission: PERMISSION_BITS.EDIT }, + { requiredPermission: permissionLevel }, { select: (res) => mapAgents(res.data), enabled: isAuthenticated, diff --git a/librechat.example.yaml b/librechat.example.yaml index 3a9230b50..4d2e7f843 100644 --- a/librechat.example.yaml +++ b/librechat.example.yaml @@ -77,6 +77,18 @@ interface: bookmarks: true multiConvo: true agents: true + peoplePicker: + admin: + users: true + groups: true + user: + users: false + groups: false + marketplace: + admin: + use: false # Enable marketplace mode for admin role + user: + use: false # Enable marketplace mode for user role # Temporary chat retention period in hours (default: 720, min: 1, max: 8760) # temporaryChatRetention: 1 diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index d7697597b..0fe7108d5 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -533,6 +533,20 @@ export const interfaceSchema = z .optional(), }) .optional(), + marketplace: z + .object({ + admin: z + .object({ + use: z.boolean().optional(), + }) + .optional(), + user: z + .object({ + use: z.boolean().optional(), + }) + .optional(), + }) + .optional(), fileSearch: z.boolean().optional(), }) .default({ @@ -559,6 +573,14 @@ export const interfaceSchema = z groups: false, }, }, + marketplace: { + admin: { + use: false, + }, + user: { + use: false, + }, + }, fileSearch: true, }); diff --git a/packages/data-provider/src/permissions.ts b/packages/data-provider/src/permissions.ts index ff32646bc..e07570076 100644 --- a/packages/data-provider/src/permissions.ts +++ b/packages/data-provider/src/permissions.ts @@ -40,6 +40,10 @@ export enum PermissionTypes { * Type for People Picker Permissions */ PEOPLE_PICKER = 'PEOPLE_PICKER', + /** + * Type for Marketplace Permissions + */ + MARKETPLACE = 'MARKETPLACE', /** * Type for using the "File Search" feature */ @@ -119,6 +123,11 @@ export const peoplePickerPermissionsSchema = z.object({ }); export type TPeoplePickerPermissions = z.infer; +export const marketplacePermissionsSchema = z.object({ + [Permissions.USE]: z.boolean().default(false), +}); +export type TMarketplacePermissions = z.infer; + export const fileSearchPermissionsSchema = z.object({ [Permissions.USE]: z.boolean().default(true), }); @@ -135,5 +144,6 @@ export const permissionsSchema = z.object({ [PermissionTypes.RUN_CODE]: runCodePermissionsSchema, [PermissionTypes.WEB_SEARCH]: webSearchPermissionsSchema, [PermissionTypes.PEOPLE_PICKER]: peoplePickerPermissionsSchema, + [PermissionTypes.MARKETPLACE]: marketplacePermissionsSchema, [PermissionTypes.FILE_SEARCH]: fileSearchPermissionsSchema, }); diff --git a/packages/data-provider/src/roles.ts b/packages/data-provider/src/roles.ts index 97484f637..5881ef775 100644 --- a/packages/data-provider/src/roles.ts +++ b/packages/data-provider/src/roles.ts @@ -80,6 +80,9 @@ const defaultRolesSchema = z.object({ [Permissions.VIEW_USERS]: z.boolean().default(true), [Permissions.VIEW_GROUPS]: z.boolean().default(true), }), + [PermissionTypes.MARKETPLACE]: z.object({ + [Permissions.USE]: z.boolean().default(false), + }), [PermissionTypes.FILE_SEARCH]: fileSearchPermissionsSchema.extend({ [Permissions.USE]: z.boolean().default(true), }), @@ -131,6 +134,9 @@ export const roleDefaults = defaultRolesSchema.parse({ [Permissions.VIEW_USERS]: true, [Permissions.VIEW_GROUPS]: true, }, + [PermissionTypes.MARKETPLACE]: { + [Permissions.USE]: true, + }, [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: true, }, @@ -151,6 +157,9 @@ export const roleDefaults = defaultRolesSchema.parse({ [Permissions.VIEW_USERS]: false, [Permissions.VIEW_GROUPS]: false, }, + [PermissionTypes.MARKETPLACE]: { + [Permissions.USE]: false, + }, [PermissionTypes.FILE_SEARCH]: {}, }, }, diff --git a/packages/data-schemas/src/schema/role.ts b/packages/data-schemas/src/schema/role.ts index 59f9197a4..3d613872f 100644 --- a/packages/data-schemas/src/schema/role.ts +++ b/packages/data-schemas/src/schema/role.ts @@ -43,6 +43,9 @@ const rolePermissionsSchema = new Schema( [Permissions.VIEW_USERS]: { type: Boolean, default: false }, [Permissions.VIEW_GROUPS]: { type: Boolean, default: false }, }, + [PermissionTypes.MARKETPLACE]: { + [Permissions.USE]: { type: Boolean, default: false }, + }, [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: { type: Boolean, default: true }, }, @@ -80,6 +83,7 @@ const roleSchema: Schema = new Schema({ [Permissions.VIEW_USERS]: false, [Permissions.VIEW_GROUPS]: false, }, + [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: false }, [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: true }, }), }, diff --git a/packages/data-schemas/src/types/role.ts b/packages/data-schemas/src/types/role.ts index 840df42f8..515b5236f 100644 --- a/packages/data-schemas/src/types/role.ts +++ b/packages/data-schemas/src/types/role.ts @@ -39,6 +39,9 @@ export interface IRole extends Document { [Permissions.VIEW_USERS]?: boolean; [Permissions.VIEW_GROUPS]?: boolean; }; + [PermissionTypes.MARKETPLACE]?: { + [Permissions.USE]?: boolean; + }; [PermissionTypes.FILE_SEARCH]?: { [Permissions.USE]?: boolean; };