diff --git a/api/models/Role.js b/api/models/Role.js index d21efee3b8..1e77cae533 100644 --- a/api/models/Role.js +++ b/api/models/Role.js @@ -4,6 +4,7 @@ const { roleDefaults, PermissionTypes, removeNullishValues, + agentPermissionsSchema, promptPermissionsSchema, bookmarkPermissionsSchema, } = require('librechat-data-provider'); @@ -71,6 +72,7 @@ const updateRoleByName = async function (roleName, updates) { }; const permissionSchemas = { + [PermissionTypes.AGENTS]: agentPermissionsSchema, [PermissionTypes.PROMPTS]: promptPermissionsSchema, [PermissionTypes.BOOKMARKS]: bookmarkPermissionsSchema, }; diff --git a/api/models/schema/roleSchema.js b/api/models/schema/roleSchema.js index ebd1d0bc4b..b0cbeb8c2c 100644 --- a/api/models/schema/roleSchema.js +++ b/api/models/schema/roleSchema.js @@ -28,6 +28,20 @@ const roleSchema = new mongoose.Schema({ default: true, }, }, + [PermissionTypes.AGENTS]: { + [Permissions.SHARED_GLOBAL]: { + type: Boolean, + default: false, + }, + [Permissions.USE]: { + type: Boolean, + default: true, + }, + [Permissions.CREATE]: { + type: Boolean, + default: true, + }, + }, }); const Role = mongoose.model('Role', roleSchema); diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js index e009b9ff9b..381c140671 100644 --- a/api/server/controllers/agents/v1.js +++ b/api/server/controllers/agents/v1.js @@ -1,5 +1,5 @@ const { nanoid } = require('nanoid'); -const { FileContext } = require('librechat-data-provider'); +const { FileContext, Constants } = require('librechat-data-provider'); const { getAgent, createAgent, @@ -9,6 +9,7 @@ const { } = require('~/models/Agent'); const { getStrategyFunctions } = require('~/server/services/Files/strategies'); const { uploadImageBuffer } = require('~/server/services/Files/process'); +const { getProjectByName } = require('~/models/Project'); const { updateAgentProjects } = require('~/models/Agent'); const { deleteFileByFilter } = require('~/models/File'); const { logger } = require('~/config'); @@ -54,16 +55,31 @@ const createAgentHandler = async (req, res) => { * @param {object} req - Express Request * @param {object} req.params - Request params * @param {string} req.params.id - Agent identifier. - * @returns {Agent} 200 - success response - application/json + * @param {object} req.user - Authenticated user information + * @param {string} req.user.id - User ID + * @returns {Promise} 200 - success response - application/json * @returns {Error} 404 - Agent not found */ const getAgentHandler = async (req, res) => { try { const id = req.params.id; - const agent = await getAgent({ id }); + const author = req.user.id; + + let query = { id, author }; + + const globalProject = await getProjectByName(Constants.GLOBAL_PROJECT_NAME, ['agentIds']); + if (globalProject && (globalProject.agentIds?.length ?? 0) > 0) { + query = { + $or: [{ id, $in: globalProject.agentIds }, query], + }; + } + + const agent = await getAgent(query); + if (!agent) { return res.status(404).json({ error: 'Agent not found' }); } + return res.status(200).json(agent); } catch (error) { logger.error('[/Agents/:id] Error retrieving agent', error); @@ -127,7 +143,7 @@ const deleteAgentHandler = async (req, res) => { * @param {object} req - Express Request * @param {object} req.query - Request query * @param {string} [req.query.user] - The user ID of the agent's author. - * @returns {AgentListResponse} 200 - success response - application/json + * @returns {Promise} 200 - success response - application/json */ const getListAgentsHandler = async (req, res) => { try { diff --git a/api/server/routes/agents/v1.js b/api/server/routes/agents/v1.js index 1001873fe4..d3a3005bd5 100644 --- a/api/server/routes/agents/v1.js +++ b/api/server/routes/agents/v1.js @@ -1,11 +1,30 @@ const multer = require('multer'); const express = require('express'); +const { PermissionTypes, Permissions } = require('librechat-data-provider'); +const { requireJwtAuth, generateCheckAccess } = require('~/server/middleware'); const v1 = require('~/server/controllers/agents/v1'); const actions = require('./actions'); const upload = multer(); const router = express.Router(); +const checkAgentAccess = generateCheckAccess(PermissionTypes.AGENTS, [Permissions.USE]); +const checkAgentCreate = generateCheckAccess(PermissionTypes.AGENTS, [ + Permissions.USE, + Permissions.CREATE, +]); + +const checkGlobalAgentShare = generateCheckAccess( + PermissionTypes.AGENTS, + [Permissions.USE, Permissions.CREATE], + { + [Permissions.SHARED_GLOBAL]: ['projectIds', 'removeProjectIds'], + }, +); + +router.use(requireJwtAuth); +router.use(checkAgentAccess); + /** * Agent actions route. * @route GET|POST /agents/actions @@ -27,7 +46,7 @@ router.use('/tools', (req, res) => { * @param {AgentCreateParams} req.body - The agent creation parameters. * @returns {Agent} 201 - Success response - application/json */ -router.post('/', v1.createAgent); +router.post('/', checkAgentCreate, v1.createAgent); /** * Retrieves an agent. @@ -35,7 +54,7 @@ router.post('/', v1.createAgent); * @param {string} req.params.id - Agent identifier. * @returns {Agent} 200 - Success response - application/json */ -router.get('/:id', v1.getAgent); +router.get('/:id', checkAgentAccess, v1.getAgent); /** * Updates an agent. @@ -44,7 +63,7 @@ router.get('/:id', v1.getAgent); * @param {AgentUpdateParams} req.body - The agent update parameters. * @returns {Agent} 200 - Success response - application/json */ -router.patch('/:id', v1.updateAgent); +router.patch('/:id', checkGlobalAgentShare, v1.updateAgent); /** * Deletes an agent. @@ -52,7 +71,7 @@ router.patch('/:id', v1.updateAgent); * @param {string} req.params.id - Agent identifier. * @returns {Agent} 200 - success response - application/json */ -router.delete('/:id', v1.deleteAgent); +router.delete('/:id', checkAgentCreate, v1.deleteAgent); /** * Returns a list of agents. @@ -60,9 +79,7 @@ router.delete('/:id', v1.deleteAgent); * @param {AgentListParams} req.query - The agent list parameters for pagination and sorting. * @returns {AgentListResponse} 200 - success response - application/json */ -router.get('/', v1.getListAgents); - -// TODO: handle private agents +router.get('/', checkAgentAccess, v1.getListAgents); /** * Uploads and updates an avatar for a specific agent. @@ -72,6 +89,6 @@ router.get('/', v1.getListAgents); * @param {string} [req.body.metadata] - Optional metadata for the agent's avatar. * @returns {Object} 200 - success response - application/json */ -router.post('/avatar/:agent_id', upload.single('file'), v1.uploadAgentAvatar); +router.post('/avatar/:agent_id', checkAgentAccess, upload.single('file'), v1.uploadAgentAvatar); module.exports = router; diff --git a/packages/data-provider/src/roles.ts b/packages/data-provider/src/roles.ts index 62ade4fcd8..52cda1be3f 100644 --- a/packages/data-provider/src/roles.ts +++ b/packages/data-provider/src/roles.ts @@ -23,9 +23,13 @@ export enum PermissionTypes { */ PROMPTS = 'PROMPTS', /** - * Type for Bookmarks Permissions + * Type for Bookmark Permissions */ BOOKMARKS = 'BOOKMARKS', + /** + * Type for Agent Permissions + */ + AGENTS = 'AGENTS', } /** @@ -49,13 +53,22 @@ export const bookmarkPermissionsSchema = z.object({ [Permissions.USE]: z.boolean().default(true), }); +export const agentPermissionsSchema = z.object({ + [Permissions.SHARED_GLOBAL]: z.boolean().default(false), + [Permissions.USE]: z.boolean().default(true), + [Permissions.CREATE]: z.boolean().default(true), + [Permissions.SHARE]: z.boolean().default(false), +}); + export const roleSchema = z.object({ name: z.string(), [PermissionTypes.PROMPTS]: promptPermissionsSchema, [PermissionTypes.BOOKMARKS]: bookmarkPermissionsSchema, + [PermissionTypes.AGENTS]: agentPermissionsSchema, }); export type TRole = z.infer; +export type TAgentPermissions = z.infer; export type TPromptPermissions = z.infer; export type TBookmarkPermissions = z.infer; @@ -71,11 +84,18 @@ const defaultRolesSchema = z.object({ [PermissionTypes.BOOKMARKS]: bookmarkPermissionsSchema.extend({ [Permissions.USE]: z.boolean().default(true), }), + [PermissionTypes.AGENTS]: agentPermissionsSchema.extend({ + [Permissions.SHARED_GLOBAL]: z.boolean().default(true), + [Permissions.USE]: z.boolean().default(true), + [Permissions.CREATE]: z.boolean().default(true), + [Permissions.SHARE]: z.boolean().default(true), + }), }), [SystemRoles.USER]: roleSchema.extend({ name: z.literal(SystemRoles.USER), [PermissionTypes.PROMPTS]: promptPermissionsSchema, [PermissionTypes.BOOKMARKS]: bookmarkPermissionsSchema, + [PermissionTypes.AGENTS]: agentPermissionsSchema, }), }); @@ -84,10 +104,12 @@ export const roleDefaults = defaultRolesSchema.parse({ name: SystemRoles.ADMIN, [PermissionTypes.PROMPTS]: {}, [PermissionTypes.BOOKMARKS]: {}, + [PermissionTypes.AGENTS]: {}, }, [SystemRoles.USER]: { name: SystemRoles.USER, [PermissionTypes.PROMPTS]: {}, [PermissionTypes.BOOKMARKS]: {}, + [PermissionTypes.AGENTS]: {}, }, });