diff --git a/api/server/services/start/interface.js b/api/server/services/start/interface.js index a8800dff51..2fb0748b9e 100644 --- a/api/server/services/start/interface.js +++ b/api/server/services/start/interface.js @@ -61,6 +61,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, { @@ -79,6 +87,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, + }, }); await updateAccessPermissions(SystemRoles.ADMIN, { [PermissionTypes.PROMPTS]: { [Permissions.USE]: loadedInterface.prompts }, @@ -96,6 +107,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, + }, }); let i = 0; diff --git a/client/src/components/Nav/NewChat.tsx b/client/src/components/Nav/NewChat.tsx index 4348a3bad7..94fb8f5772 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 78740c7edd..3557a01197 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 bd8ce58601..3f2da4a0cc 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 0fb35beebf..5f253f6c86 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 d872e6f34d..112f75a250 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 0fb7975cbb..5665bf57a9 100644 --- a/librechat.example.yaml +++ b/librechat.example.yaml @@ -73,6 +73,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 255f36ed78..117de8ce1f 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -529,6 +529,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(), }) .default({ endpointsMenu: true, @@ -554,6 +568,14 @@ export const interfaceSchema = z groups: false, }, }, + marketplace: { + admin: { + use: false, + }, + user: { + use: false, + }, + }, }); export type TInterfaceConfig = z.infer; diff --git a/packages/data-provider/src/permissions.ts b/packages/data-provider/src/permissions.ts index be8a95ed6a..219bd0c80a 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', } /** @@ -115,6 +119,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; + // Define a single permissions schema that holds all permission types. export const permissionsSchema = z.object({ [PermissionTypes.PROMPTS]: promptPermissionsSchema, @@ -126,4 +135,5 @@ export const permissionsSchema = z.object({ [PermissionTypes.RUN_CODE]: runCodePermissionsSchema, [PermissionTypes.WEB_SEARCH]: webSearchPermissionsSchema, [PermissionTypes.PEOPLE_PICKER]: peoplePickerPermissionsSchema, + [PermissionTypes.MARKETPLACE]: marketplacePermissionsSchema, }); diff --git a/packages/data-provider/src/roles.ts b/packages/data-provider/src/roles.ts index 488d524207..2f35b6574f 100644 --- a/packages/data-provider/src/roles.ts +++ b/packages/data-provider/src/roles.ts @@ -79,6 +79,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), + }), }), }), [SystemRoles.USER]: roleSchema.extend({ @@ -127,6 +130,9 @@ export const roleDefaults = defaultRolesSchema.parse({ [Permissions.VIEW_USERS]: true, [Permissions.VIEW_GROUPS]: true, }, + [PermissionTypes.MARKETPLACE]: { + [Permissions.USE]: true, + }, }, }, [SystemRoles.USER]: { @@ -144,6 +150,9 @@ export const roleDefaults = defaultRolesSchema.parse({ [Permissions.VIEW_USERS]: false, [Permissions.VIEW_GROUPS]: false, }, + [PermissionTypes.MARKETPLACE]: { + [Permissions.USE]: false, + }, }, }, }); diff --git a/packages/data-schemas/src/schema/role.ts b/packages/data-schemas/src/schema/role.ts index 3147d6c287..836f0f42d4 100644 --- a/packages/data-schemas/src/schema/role.ts +++ b/packages/data-schemas/src/schema/role.ts @@ -41,6 +41,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 }, + }, }, { _id: false }, ); @@ -75,6 +78,7 @@ const roleSchema: Schema = new Schema({ [Permissions.VIEW_USERS]: false, [Permissions.VIEW_GROUPS]: false, }, + [PermissionTypes.MARKETPLACE]: { [Permissions.USE]: false }, }), }, }); diff --git a/packages/data-schemas/src/types/role.ts b/packages/data-schemas/src/types/role.ts index 784e45e9b0..db418d1f14 100644 --- a/packages/data-schemas/src/types/role.ts +++ b/packages/data-schemas/src/types/role.ts @@ -39,5 +39,8 @@ export interface IRole extends Document { [Permissions.VIEW_USERS]?: boolean; [Permissions.VIEW_GROUPS]?: boolean; }; + [PermissionTypes.MARKETPLACE]?: { + [Permissions.USE]?: boolean; + }; }; }