mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 17:00:15 +01:00
🔒 fix: Agents Config/Permission Checks after Streamline Change (#8089)
* refactor: access control logic to TypeScript * chore: Change EndpointURLs to a constant object for improved type safety * 🐛 fix: Enhance agent access control by adding skipAgentCheck functionality * 🐛 fix: Add endpointFileConfig prop to AttachFileMenu and update file handling logic * 🐛 fix: Update tool handling logic to support optional groupedTools and improve null checks, add dedicated tool dialog for Assistants * chore: Export Accordion component from UI index for improved modularity * feat: Add ActivePanelContext for managing active panel state across components * chore: Replace string IDs with EModelEndpoint constants for assistants and agents in useSideNavLinks * fix: Integrate access checks for agent creation and deletion routes in actions.js
This commit is contained in:
parent
d18b2c3f1f
commit
02c7f744ba
31 changed files with 672 additions and 242 deletions
|
|
@ -4,6 +4,7 @@ const {
|
||||||
sendEvent,
|
sendEvent,
|
||||||
createRun,
|
createRun,
|
||||||
Tokenizer,
|
Tokenizer,
|
||||||
|
checkAccess,
|
||||||
memoryInstructions,
|
memoryInstructions,
|
||||||
createMemoryProcessor,
|
createMemoryProcessor,
|
||||||
} = require('@librechat/api');
|
} = require('@librechat/api');
|
||||||
|
|
@ -39,8 +40,8 @@ const { spendTokens, spendStructuredTokens } = require('~/models/spendTokens');
|
||||||
const { getFormattedMemories, deleteMemory, setMemory } = require('~/models');
|
const { getFormattedMemories, deleteMemory, setMemory } = require('~/models');
|
||||||
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
|
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
|
||||||
const { getProviderConfig } = require('~/server/services/Endpoints');
|
const { getProviderConfig } = require('~/server/services/Endpoints');
|
||||||
const { checkAccess } = require('~/server/middleware/roles/access');
|
|
||||||
const BaseClient = require('~/app/clients/BaseClient');
|
const BaseClient = require('~/app/clients/BaseClient');
|
||||||
|
const { getRoleByName } = require('~/models/Role');
|
||||||
const { loadAgent } = require('~/models/Agent');
|
const { loadAgent } = require('~/models/Agent');
|
||||||
const { getMCPManager } = require('~/config');
|
const { getMCPManager } = require('~/config');
|
||||||
|
|
||||||
|
|
@ -401,7 +402,12 @@ class AgentClient extends BaseClient {
|
||||||
if (user.personalization?.memories === false) {
|
if (user.personalization?.memories === false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const hasAccess = await checkAccess(user, PermissionTypes.MEMORIES, [Permissions.USE]);
|
const hasAccess = await checkAccess({
|
||||||
|
user,
|
||||||
|
permissionType: PermissionTypes.MEMORIES,
|
||||||
|
permissions: [Permissions.USE],
|
||||||
|
getRoleByName,
|
||||||
|
});
|
||||||
|
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
const { nanoid } = require('nanoid');
|
const { nanoid } = require('nanoid');
|
||||||
const { EnvVar } = require('@librechat/agents');
|
const { EnvVar } = require('@librechat/agents');
|
||||||
|
const { checkAccess } = require('@librechat/api');
|
||||||
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const {
|
const {
|
||||||
Tools,
|
Tools,
|
||||||
AuthType,
|
AuthType,
|
||||||
|
|
@ -13,9 +15,8 @@ const { processCodeOutput } = require('~/server/services/Files/Code/process');
|
||||||
const { createToolCall, getToolCallsByConvo } = require('~/models/ToolCall');
|
const { createToolCall, getToolCallsByConvo } = require('~/models/ToolCall');
|
||||||
const { loadAuthValues } = require('~/server/services/Tools/credentials');
|
const { loadAuthValues } = require('~/server/services/Tools/credentials');
|
||||||
const { loadTools } = require('~/app/clients/tools/util');
|
const { loadTools } = require('~/app/clients/tools/util');
|
||||||
const { checkAccess } = require('~/server/middleware');
|
const { getRoleByName } = require('~/models/Role');
|
||||||
const { getMessage } = require('~/models/Message');
|
const { getMessage } = require('~/models/Message');
|
||||||
const { logger } = require('~/config');
|
|
||||||
|
|
||||||
const fieldsMap = {
|
const fieldsMap = {
|
||||||
[Tools.execute_code]: [EnvVar.CODE_API_KEY],
|
[Tools.execute_code]: [EnvVar.CODE_API_KEY],
|
||||||
|
|
@ -79,6 +80,7 @@ const verifyToolAuth = async (req, res) => {
|
||||||
throwError: false,
|
throwError: false,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
logger.error('Error loading auth values', error);
|
||||||
res.status(200).json({ authenticated: false, message: AuthType.USER_PROVIDED });
|
res.status(200).json({ authenticated: false, message: AuthType.USER_PROVIDED });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -132,7 +134,12 @@ const callTool = async (req, res) => {
|
||||||
logger.debug(`[${toolId}/call] User: ${req.user.id}`);
|
logger.debug(`[${toolId}/call] User: ${req.user.id}`);
|
||||||
let hasAccess = true;
|
let hasAccess = true;
|
||||||
if (toolAccessPermType[toolId]) {
|
if (toolAccessPermType[toolId]) {
|
||||||
hasAccess = await checkAccess(req.user, toolAccessPermType[toolId], [Permissions.USE]);
|
hasAccess = await checkAccess({
|
||||||
|
user: req.user,
|
||||||
|
permissionType: toolAccessPermType[toolId],
|
||||||
|
permissions: [Permissions.USE],
|
||||||
|
getRoleByName,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
|
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
const { getRoleByName } = require('~/models/Role');
|
|
||||||
const { logger } = require('~/config');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Core function to check if a user has one or more required permissions
|
|
||||||
*
|
|
||||||
* @param {object} user - The user object
|
|
||||||
* @param {PermissionTypes} permissionType - The type of permission to check
|
|
||||||
* @param {Permissions[]} permissions - The list of specific permissions to check
|
|
||||||
* @param {Record<Permissions, string[]>} [bodyProps] - An optional object where keys are permissions and values are arrays of properties to check
|
|
||||||
* @param {object} [checkObject] - The object to check properties against
|
|
||||||
* @returns {Promise<boolean>} Whether the user has the required permissions
|
|
||||||
*/
|
|
||||||
const checkAccess = async (user, permissionType, permissions, bodyProps = {}, checkObject = {}) => {
|
|
||||||
if (!user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const role = await getRoleByName(user.role);
|
|
||||||
if (role && role.permissions && role.permissions[permissionType]) {
|
|
||||||
const hasAnyPermission = permissions.some((permission) => {
|
|
||||||
if (role.permissions[permissionType][permission]) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bodyProps[permission] && checkObject) {
|
|
||||||
return bodyProps[permission].some((prop) =>
|
|
||||||
Object.prototype.hasOwnProperty.call(checkObject, prop),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
return hasAnyPermission;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Middleware to check if a user has one or more required permissions, optionally based on `req.body` properties.
|
|
||||||
*
|
|
||||||
* @param {PermissionTypes} permissionType - The type of permission to check.
|
|
||||||
* @param {Permissions[]} permissions - The list of specific permissions to check.
|
|
||||||
* @param {Record<Permissions, string[]>} [bodyProps] - An optional object where keys are permissions and values are arrays of `req.body` properties to check.
|
|
||||||
* @returns {(req: ServerRequest, res: ServerResponse, next: NextFunction) => Promise<void>} Express middleware function.
|
|
||||||
*/
|
|
||||||
const generateCheckAccess = (permissionType, permissions, bodyProps = {}) => {
|
|
||||||
return async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const hasAccess = await checkAccess(
|
|
||||||
req.user,
|
|
||||||
permissionType,
|
|
||||||
permissions,
|
|
||||||
bodyProps,
|
|
||||||
req.body,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasAccess) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.warn(
|
|
||||||
`[${permissionType}] Forbidden: Insufficient permissions for User ${req.user.id}: ${permissions.join(', ')}`,
|
|
||||||
);
|
|
||||||
return res.status(403).json({ message: 'Forbidden: Insufficient permissions' });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error);
|
|
||||||
return res.status(500).json({ message: `Server error: ${error.message}` });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
checkAccess,
|
|
||||||
generateCheckAccess,
|
|
||||||
};
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
const checkAdmin = require('./admin');
|
const checkAdmin = require('./admin');
|
||||||
const { checkAccess, generateCheckAccess } = require('./access');
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
checkAdmin,
|
checkAdmin,
|
||||||
checkAccess,
|
|
||||||
generateCheckAccess,
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,28 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { nanoid } = require('nanoid');
|
const { nanoid } = require('nanoid');
|
||||||
const { actionDelimiter, SystemRoles, removeNullishValues } = require('librechat-data-provider');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
|
const { generateCheckAccess } = require('@librechat/api');
|
||||||
|
const {
|
||||||
|
SystemRoles,
|
||||||
|
Permissions,
|
||||||
|
PermissionTypes,
|
||||||
|
actionDelimiter,
|
||||||
|
removeNullishValues,
|
||||||
|
} = require('librechat-data-provider');
|
||||||
const { encryptMetadata, domainParser } = require('~/server/services/ActionService');
|
const { encryptMetadata, domainParser } = require('~/server/services/ActionService');
|
||||||
const { updateAction, getActions, deleteAction } = require('~/models/Action');
|
const { updateAction, getActions, deleteAction } = require('~/models/Action');
|
||||||
const { isActionDomainAllowed } = require('~/server/services/domains');
|
const { isActionDomainAllowed } = require('~/server/services/domains');
|
||||||
const { getAgent, updateAgent } = require('~/models/Agent');
|
const { getAgent, updateAgent } = require('~/models/Agent');
|
||||||
const { logger } = require('~/config');
|
const { getRoleByName } = require('~/models/Role');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
const checkAgentCreate = generateCheckAccess({
|
||||||
|
permissionType: PermissionTypes.AGENTS,
|
||||||
|
permissions: [Permissions.USE, Permissions.CREATE],
|
||||||
|
getRoleByName,
|
||||||
|
});
|
||||||
|
|
||||||
// If the user has ADMIN role
|
// If the user has ADMIN role
|
||||||
// then action edition is possible even if not owner of the assistant
|
// then action edition is possible even if not owner of the assistant
|
||||||
const isAdmin = (req) => {
|
const isAdmin = (req) => {
|
||||||
|
|
@ -41,7 +55,7 @@ router.get('/', async (req, res) => {
|
||||||
* @param {ActionMetadata} req.body.metadata - Metadata for the action.
|
* @param {ActionMetadata} req.body.metadata - Metadata for the action.
|
||||||
* @returns {Object} 200 - success response - application/json
|
* @returns {Object} 200 - success response - application/json
|
||||||
*/
|
*/
|
||||||
router.post('/:agent_id', async (req, res) => {
|
router.post('/:agent_id', checkAgentCreate, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { agent_id } = req.params;
|
const { agent_id } = req.params;
|
||||||
|
|
||||||
|
|
@ -149,7 +163,7 @@ router.post('/:agent_id', async (req, res) => {
|
||||||
* @param {string} req.params.action_id - The ID of the action to delete.
|
* @param {string} req.params.action_id - The ID of the action to delete.
|
||||||
* @returns {Object} 200 - success response - application/json
|
* @returns {Object} 200 - success response - application/json
|
||||||
*/
|
*/
|
||||||
router.delete('/:agent_id/:action_id', async (req, res) => {
|
router.delete('/:agent_id/:action_id', checkAgentCreate, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { agent_id, action_id } = req.params;
|
const { agent_id, action_id } = req.params;
|
||||||
const admin = isAdmin(req);
|
const admin = isAdmin(req);
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,28 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const { generateCheckAccess, skipAgentCheck } = require('@librechat/api');
|
||||||
const { PermissionTypes, Permissions } = require('librechat-data-provider');
|
const { PermissionTypes, Permissions } = require('librechat-data-provider');
|
||||||
const {
|
const {
|
||||||
setHeaders,
|
setHeaders,
|
||||||
moderateText,
|
moderateText,
|
||||||
// validateModel,
|
// validateModel,
|
||||||
generateCheckAccess,
|
|
||||||
validateConvoAccess,
|
validateConvoAccess,
|
||||||
buildEndpointOption,
|
buildEndpointOption,
|
||||||
} = require('~/server/middleware');
|
} = require('~/server/middleware');
|
||||||
const { initializeClient } = require('~/server/services/Endpoints/agents');
|
const { initializeClient } = require('~/server/services/Endpoints/agents');
|
||||||
const AgentController = require('~/server/controllers/agents/request');
|
const AgentController = require('~/server/controllers/agents/request');
|
||||||
const addTitle = require('~/server/services/Endpoints/agents/title');
|
const addTitle = require('~/server/services/Endpoints/agents/title');
|
||||||
|
const { getRoleByName } = require('~/models/Role');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.use(moderateText);
|
router.use(moderateText);
|
||||||
|
|
||||||
const checkAgentAccess = generateCheckAccess(PermissionTypes.AGENTS, [Permissions.USE]);
|
const checkAgentAccess = generateCheckAccess({
|
||||||
|
permissionType: PermissionTypes.AGENTS,
|
||||||
|
permissions: [Permissions.USE],
|
||||||
|
skipCheck: skipAgentCheck,
|
||||||
|
getRoleByName,
|
||||||
|
});
|
||||||
|
|
||||||
router.use(checkAgentAccess);
|
router.use(checkAgentAccess);
|
||||||
router.use(validateConvoAccess);
|
router.use(validateConvoAccess);
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,36 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const { generateCheckAccess } = require('@librechat/api');
|
||||||
const { PermissionTypes, Permissions } = require('librechat-data-provider');
|
const { PermissionTypes, Permissions } = require('librechat-data-provider');
|
||||||
const { requireJwtAuth, generateCheckAccess } = require('~/server/middleware');
|
const { requireJwtAuth } = require('~/server/middleware');
|
||||||
const v1 = require('~/server/controllers/agents/v1');
|
const v1 = require('~/server/controllers/agents/v1');
|
||||||
|
const { getRoleByName } = require('~/models/Role');
|
||||||
const actions = require('./actions');
|
const actions = require('./actions');
|
||||||
const tools = require('./tools');
|
const tools = require('./tools');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const avatar = express.Router();
|
const avatar = express.Router();
|
||||||
|
|
||||||
const checkAgentAccess = generateCheckAccess(PermissionTypes.AGENTS, [Permissions.USE]);
|
const checkAgentAccess = generateCheckAccess({
|
||||||
const checkAgentCreate = generateCheckAccess(PermissionTypes.AGENTS, [
|
permissionType: PermissionTypes.AGENTS,
|
||||||
Permissions.USE,
|
permissions: [Permissions.USE],
|
||||||
Permissions.CREATE,
|
getRoleByName,
|
||||||
]);
|
});
|
||||||
|
const checkAgentCreate = generateCheckAccess({
|
||||||
|
permissionType: PermissionTypes.AGENTS,
|
||||||
|
permissions: [Permissions.USE, Permissions.CREATE],
|
||||||
|
getRoleByName,
|
||||||
|
});
|
||||||
|
|
||||||
const checkGlobalAgentShare = generateCheckAccess(
|
const checkGlobalAgentShare = generateCheckAccess({
|
||||||
PermissionTypes.AGENTS,
|
permissionType: PermissionTypes.AGENTS,
|
||||||
[Permissions.USE, Permissions.CREATE],
|
permissions: [Permissions.USE, Permissions.CREATE],
|
||||||
{
|
bodyProps: {
|
||||||
[Permissions.SHARED_GLOBAL]: ['projectIds', 'removeProjectIds'],
|
[Permissions.SHARED_GLOBAL]: ['projectIds', 'removeProjectIds'],
|
||||||
},
|
},
|
||||||
);
|
getRoleByName,
|
||||||
|
});
|
||||||
|
|
||||||
router.use(requireJwtAuth);
|
router.use(requireJwtAuth);
|
||||||
router.use(checkAgentAccess);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Agent actions route.
|
* Agent actions route.
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,43 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { Tokenizer } = require('@librechat/api');
|
const { Tokenizer, generateCheckAccess } = require('@librechat/api');
|
||||||
const { PermissionTypes, Permissions } = require('librechat-data-provider');
|
const { PermissionTypes, Permissions } = require('librechat-data-provider');
|
||||||
const {
|
const {
|
||||||
getAllUserMemories,
|
getAllUserMemories,
|
||||||
toggleUserMemories,
|
toggleUserMemories,
|
||||||
createMemory,
|
createMemory,
|
||||||
setMemory,
|
|
||||||
deleteMemory,
|
deleteMemory,
|
||||||
|
setMemory,
|
||||||
} = require('~/models');
|
} = require('~/models');
|
||||||
const { requireJwtAuth, generateCheckAccess } = require('~/server/middleware');
|
const { requireJwtAuth } = require('~/server/middleware');
|
||||||
|
const { getRoleByName } = require('~/models/Role');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const checkMemoryRead = generateCheckAccess(PermissionTypes.MEMORIES, [
|
const checkMemoryRead = generateCheckAccess({
|
||||||
Permissions.USE,
|
permissionType: PermissionTypes.MEMORIES,
|
||||||
Permissions.READ,
|
permissions: [Permissions.USE, Permissions.READ],
|
||||||
]);
|
getRoleByName,
|
||||||
const checkMemoryCreate = generateCheckAccess(PermissionTypes.MEMORIES, [
|
});
|
||||||
Permissions.USE,
|
const checkMemoryCreate = generateCheckAccess({
|
||||||
Permissions.CREATE,
|
permissionType: PermissionTypes.MEMORIES,
|
||||||
]);
|
permissions: [Permissions.USE, Permissions.CREATE],
|
||||||
const checkMemoryUpdate = generateCheckAccess(PermissionTypes.MEMORIES, [
|
getRoleByName,
|
||||||
Permissions.USE,
|
});
|
||||||
Permissions.UPDATE,
|
const checkMemoryUpdate = generateCheckAccess({
|
||||||
]);
|
permissionType: PermissionTypes.MEMORIES,
|
||||||
const checkMemoryDelete = generateCheckAccess(PermissionTypes.MEMORIES, [
|
permissions: [Permissions.USE, Permissions.UPDATE],
|
||||||
Permissions.USE,
|
getRoleByName,
|
||||||
Permissions.UPDATE,
|
});
|
||||||
]);
|
const checkMemoryDelete = generateCheckAccess({
|
||||||
const checkMemoryOptOut = generateCheckAccess(PermissionTypes.MEMORIES, [
|
permissionType: PermissionTypes.MEMORIES,
|
||||||
Permissions.USE,
|
permissions: [Permissions.USE, Permissions.UPDATE],
|
||||||
Permissions.OPT_OUT,
|
getRoleByName,
|
||||||
]);
|
});
|
||||||
|
const checkMemoryOptOut = generateCheckAccess({
|
||||||
|
permissionType: PermissionTypes.MEMORIES,
|
||||||
|
permissions: [Permissions.USE, Permissions.OPT_OUT],
|
||||||
|
getRoleByName,
|
||||||
|
});
|
||||||
|
|
||||||
router.use(requireJwtAuth);
|
router.use(requireJwtAuth);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { PermissionTypes, Permissions, SystemRoles } = require('librechat-data-provider');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
|
const { generateCheckAccess } = require('@librechat/api');
|
||||||
|
const { Permissions, SystemRoles, PermissionTypes } = require('librechat-data-provider');
|
||||||
const {
|
const {
|
||||||
getPrompt,
|
getPrompt,
|
||||||
getPrompts,
|
getPrompts,
|
||||||
|
|
@ -14,24 +16,30 @@ const {
|
||||||
// updatePromptLabels,
|
// updatePromptLabels,
|
||||||
makePromptProduction,
|
makePromptProduction,
|
||||||
} = require('~/models/Prompt');
|
} = require('~/models/Prompt');
|
||||||
const { requireJwtAuth, generateCheckAccess } = require('~/server/middleware');
|
const { requireJwtAuth } = require('~/server/middleware');
|
||||||
const { logger } = require('~/config');
|
const { getRoleByName } = require('~/models/Role');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const checkPromptAccess = generateCheckAccess(PermissionTypes.PROMPTS, [Permissions.USE]);
|
const checkPromptAccess = generateCheckAccess({
|
||||||
const checkPromptCreate = generateCheckAccess(PermissionTypes.PROMPTS, [
|
permissionType: PermissionTypes.PROMPTS,
|
||||||
Permissions.USE,
|
permissions: [Permissions.USE],
|
||||||
Permissions.CREATE,
|
getRoleByName,
|
||||||
]);
|
});
|
||||||
|
const checkPromptCreate = generateCheckAccess({
|
||||||
|
permissionType: PermissionTypes.PROMPTS,
|
||||||
|
permissions: [Permissions.USE, Permissions.CREATE],
|
||||||
|
getRoleByName,
|
||||||
|
});
|
||||||
|
|
||||||
const checkGlobalPromptShare = generateCheckAccess(
|
const checkGlobalPromptShare = generateCheckAccess({
|
||||||
PermissionTypes.PROMPTS,
|
permissionType: PermissionTypes.PROMPTS,
|
||||||
[Permissions.USE, Permissions.CREATE],
|
permissions: [Permissions.USE, Permissions.CREATE],
|
||||||
{
|
bodyProps: {
|
||||||
[Permissions.SHARED_GLOBAL]: ['projectIds', 'removeProjectIds'],
|
[Permissions.SHARED_GLOBAL]: ['projectIds', 'removeProjectIds'],
|
||||||
},
|
},
|
||||||
);
|
getRoleByName,
|
||||||
|
});
|
||||||
|
|
||||||
router.use(requireJwtAuth);
|
router.use(requireJwtAuth);
|
||||||
router.use(checkPromptAccess);
|
router.use(checkPromptAccess);
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,24 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const { logger } = require('@librechat/data-schemas');
|
||||||
|
const { generateCheckAccess } = require('@librechat/api');
|
||||||
const { PermissionTypes, Permissions } = require('librechat-data-provider');
|
const { PermissionTypes, Permissions } = require('librechat-data-provider');
|
||||||
const {
|
const {
|
||||||
getConversationTags,
|
updateTagsForConversation,
|
||||||
updateConversationTag,
|
updateConversationTag,
|
||||||
createConversationTag,
|
createConversationTag,
|
||||||
deleteConversationTag,
|
deleteConversationTag,
|
||||||
updateTagsForConversation,
|
getConversationTags,
|
||||||
} = require('~/models/ConversationTag');
|
} = require('~/models/ConversationTag');
|
||||||
const { requireJwtAuth, generateCheckAccess } = require('~/server/middleware');
|
const { requireJwtAuth } = require('~/server/middleware');
|
||||||
const { logger } = require('~/config');
|
const { getRoleByName } = require('~/models/Role');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const checkBookmarkAccess = generateCheckAccess(PermissionTypes.BOOKMARKS, [Permissions.USE]);
|
const checkBookmarkAccess = generateCheckAccess({
|
||||||
|
permissionType: PermissionTypes.BOOKMARKS,
|
||||||
|
permissions: [Permissions.USE],
|
||||||
|
getRoleByName,
|
||||||
|
});
|
||||||
|
|
||||||
router.use(requireJwtAuth);
|
router.use(requireJwtAuth);
|
||||||
router.use(checkBookmarkAccess);
|
router.use(checkBookmarkAccess);
|
||||||
|
|
|
||||||
37
client/src/Providers/ActivePanelContext.tsx
Normal file
37
client/src/Providers/ActivePanelContext.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { createContext, useContext, useState, ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface ActivePanelContextType {
|
||||||
|
active: string | undefined;
|
||||||
|
setActive: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ActivePanelContext = createContext<ActivePanelContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export function ActivePanelProvider({
|
||||||
|
children,
|
||||||
|
defaultActive,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
defaultActive?: string;
|
||||||
|
}) {
|
||||||
|
const [active, _setActive] = useState<string | undefined>(defaultActive);
|
||||||
|
|
||||||
|
const setActive = (id: string) => {
|
||||||
|
localStorage.setItem('side:active-panel', id);
|
||||||
|
_setActive(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActivePanelContext.Provider value={{ active, setActive }}>
|
||||||
|
{children}
|
||||||
|
</ActivePanelContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useActivePanel() {
|
||||||
|
const context = useContext(ActivePanelContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useActivePanel must be used within an ActivePanelProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
@ -40,41 +40,40 @@ export function AgentPanelProvider({ children }: { children: React.ReactNode })
|
||||||
agent_id: agent_id || '',
|
agent_id: agent_id || '',
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
const groupedTools =
|
const groupedTools = tools?.reduce(
|
||||||
tools?.reduce(
|
(acc, tool) => {
|
||||||
(acc, tool) => {
|
if (tool.tool_id.includes(Constants.mcp_delimiter)) {
|
||||||
if (tool.tool_id.includes(Constants.mcp_delimiter)) {
|
const [_toolName, serverName] = tool.tool_id.split(Constants.mcp_delimiter);
|
||||||
const [_toolName, serverName] = tool.tool_id.split(Constants.mcp_delimiter);
|
const groupKey = `${serverName.toLowerCase()}`;
|
||||||
const groupKey = `${serverName.toLowerCase()}`;
|
if (!acc[groupKey]) {
|
||||||
if (!acc[groupKey]) {
|
acc[groupKey] = {
|
||||||
acc[groupKey] = {
|
tool_id: groupKey,
|
||||||
tool_id: groupKey,
|
metadata: {
|
||||||
metadata: {
|
name: `${serverName}`,
|
||||||
name: `${serverName}`,
|
pluginKey: groupKey,
|
||||||
pluginKey: groupKey,
|
description: `${localize('com_ui_tool_collection_prefix')} ${serverName}`,
|
||||||
description: `${localize('com_ui_tool_collection_prefix')} ${serverName}`,
|
icon: tool.metadata.icon || '',
|
||||||
icon: tool.metadata.icon || '',
|
} as TPlugin,
|
||||||
} as TPlugin,
|
|
||||||
agent_id: agent_id || '',
|
|
||||||
tools: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
acc[groupKey].tools?.push({
|
|
||||||
tool_id: tool.tool_id,
|
|
||||||
metadata: tool.metadata,
|
|
||||||
agent_id: agent_id || '',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
acc[tool.tool_id] = {
|
|
||||||
tool_id: tool.tool_id,
|
|
||||||
metadata: tool.metadata,
|
|
||||||
agent_id: agent_id || '',
|
agent_id: agent_id || '',
|
||||||
|
tools: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return acc;
|
acc[groupKey].tools?.push({
|
||||||
},
|
tool_id: tool.tool_id,
|
||||||
{} as Record<string, AgentToolType & { tools?: AgentToolType[] }>,
|
metadata: tool.metadata,
|
||||||
) || {};
|
agent_id: agent_id || '',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
acc[tool.tool_id] = {
|
||||||
|
tool_id: tool.tool_id,
|
||||||
|
metadata: tool.metadata,
|
||||||
|
agent_id: agent_id || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, AgentToolType & { tools?: AgentToolType[] }>,
|
||||||
|
);
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
action,
|
action,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
export { default as AssistantsProvider } from './AssistantsContext';
|
export { default as AssistantsProvider } from './AssistantsContext';
|
||||||
export { default as AgentsProvider } from './AgentsContext';
|
export { default as AgentsProvider } from './AgentsContext';
|
||||||
export { default as ToastProvider } from './ToastContext';
|
export { default as ToastProvider } from './ToastContext';
|
||||||
|
export * from './ActivePanelContext';
|
||||||
export * from './AgentPanelContext';
|
export * from './AgentPanelContext';
|
||||||
export * from './ChatContext';
|
export * from './ChatContext';
|
||||||
export * from './ShareContext';
|
export * from './ShareContext';
|
||||||
|
|
|
||||||
|
|
@ -229,11 +229,11 @@ export type AgentPanelContextType = {
|
||||||
mcps?: t.MCP[];
|
mcps?: t.MCP[];
|
||||||
setMcp: React.Dispatch<React.SetStateAction<t.MCP | undefined>>;
|
setMcp: React.Dispatch<React.SetStateAction<t.MCP | undefined>>;
|
||||||
setMcps: React.Dispatch<React.SetStateAction<t.MCP[] | undefined>>;
|
setMcps: React.Dispatch<React.SetStateAction<t.MCP[] | undefined>>;
|
||||||
groupedTools: Record<string, t.AgentToolType & { tools?: t.AgentToolType[] }>;
|
|
||||||
tools: t.AgentToolType[];
|
tools: t.AgentToolType[];
|
||||||
activePanel?: string;
|
activePanel?: string;
|
||||||
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
|
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
|
||||||
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||||
|
groupedTools?: Record<string, t.AgentToolType & { tools?: t.AgentToolType[] }>;
|
||||||
agent_id?: string;
|
agent_id?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@ import {
|
||||||
supportsFiles,
|
supportsFiles,
|
||||||
mergeFileConfig,
|
mergeFileConfig,
|
||||||
isAgentsEndpoint,
|
isAgentsEndpoint,
|
||||||
EndpointFileConfig,
|
|
||||||
fileConfig as defaultFileConfig,
|
fileConfig as defaultFileConfig,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
|
import type { EndpointFileConfig } from 'librechat-data-provider';
|
||||||
import { useGetFileConfig } from '~/data-provider';
|
import { useGetFileConfig } from '~/data-provider';
|
||||||
import AttachFileMenu from './AttachFileMenu';
|
import AttachFileMenu from './AttachFileMenu';
|
||||||
import { useChatContext } from '~/Providers';
|
import { useChatContext } from '~/Providers';
|
||||||
|
|
@ -14,22 +14,25 @@ import { useChatContext } from '~/Providers';
|
||||||
function AttachFileChat({ disableInputs }: { disableInputs: boolean }) {
|
function AttachFileChat({ disableInputs }: { disableInputs: boolean }) {
|
||||||
const { conversation } = useChatContext();
|
const { conversation } = useChatContext();
|
||||||
const conversationId = conversation?.conversationId ?? Constants.NEW_CONVO;
|
const conversationId = conversation?.conversationId ?? Constants.NEW_CONVO;
|
||||||
const { endpoint: _endpoint, endpointType } = conversation ?? { endpoint: null };
|
const { endpoint, endpointType } = conversation ?? { endpoint: null };
|
||||||
const isAgents = useMemo(() => isAgentsEndpoint(_endpoint), [_endpoint]);
|
const isAgents = useMemo(() => isAgentsEndpoint(endpoint), [endpoint]);
|
||||||
|
|
||||||
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
|
const { data: fileConfig = defaultFileConfig } = useGetFileConfig({
|
||||||
select: (data) => mergeFileConfig(data),
|
select: (data) => mergeFileConfig(data),
|
||||||
});
|
});
|
||||||
|
|
||||||
const endpointFileConfig = fileConfig.endpoints[_endpoint ?? ''] as
|
const endpointFileConfig = fileConfig.endpoints[endpoint ?? ''] as EndpointFileConfig | undefined;
|
||||||
| EndpointFileConfig
|
const endpointSupportsFiles: boolean = supportsFiles[endpointType ?? endpoint ?? ''] ?? false;
|
||||||
| undefined;
|
|
||||||
|
|
||||||
const endpointSupportsFiles: boolean = supportsFiles[endpointType ?? _endpoint ?? ''] ?? false;
|
|
||||||
const isUploadDisabled = (disableInputs || endpointFileConfig?.disabled) ?? false;
|
const isUploadDisabled = (disableInputs || endpointFileConfig?.disabled) ?? false;
|
||||||
|
|
||||||
if (isAgents || (endpointSupportsFiles && !isUploadDisabled)) {
|
if (isAgents || (endpointSupportsFiles && !isUploadDisabled)) {
|
||||||
return <AttachFileMenu disabled={disableInputs} conversationId={conversationId} />;
|
return (
|
||||||
|
<AttachFileMenu
|
||||||
|
disabled={disableInputs}
|
||||||
|
conversationId={conversationId}
|
||||||
|
endpointFileConfig={endpointFileConfig}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { useSetRecoilState } from 'recoil';
|
||||||
import * as Ariakit from '@ariakit/react';
|
import * as Ariakit from '@ariakit/react';
|
||||||
import React, { useRef, useState, useMemo } from 'react';
|
import React, { useRef, useState, useMemo } from 'react';
|
||||||
import { FileSearch, ImageUpIcon, TerminalSquareIcon, FileType2Icon } from 'lucide-react';
|
import { FileSearch, ImageUpIcon, TerminalSquareIcon, FileType2Icon } from 'lucide-react';
|
||||||
|
import type { EndpointFileConfig } from 'librechat-data-provider';
|
||||||
import { FileUpload, TooltipAnchor, DropdownPopup, AttachmentIcon } from '~/components';
|
import { FileUpload, TooltipAnchor, DropdownPopup, AttachmentIcon } from '~/components';
|
||||||
import { EToolResources, EModelEndpoint } from 'librechat-data-provider';
|
import { EToolResources, EModelEndpoint } from 'librechat-data-provider';
|
||||||
import { useGetEndpointsQuery } from '~/data-provider';
|
import { useGetEndpointsQuery } from '~/data-provider';
|
||||||
|
|
@ -12,9 +13,10 @@ import { cn } from '~/utils';
|
||||||
interface AttachFileMenuProps {
|
interface AttachFileMenuProps {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
disabled?: boolean | null;
|
disabled?: boolean | null;
|
||||||
|
endpointFileConfig?: EndpointFileConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AttachFileMenu = ({ disabled, conversationId }: AttachFileMenuProps) => {
|
const AttachFileMenu = ({ disabled, conversationId, endpointFileConfig }: AttachFileMenuProps) => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const isUploadDisabled = disabled ?? false;
|
const isUploadDisabled = disabled ?? false;
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
@ -24,6 +26,7 @@ const AttachFileMenu = ({ disabled, conversationId }: AttachFileMenuProps) => {
|
||||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||||
const { handleFileChange } = useFileHandling({
|
const { handleFileChange } = useFileHandling({
|
||||||
overrideEndpoint: EModelEndpoint.agents,
|
overrideEndpoint: EModelEndpoint.agents,
|
||||||
|
overrideEndpointFileConfig: endpointFileConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
/** TODO: Ephemeral Agent Capabilities
|
/** TODO: Ephemeral Agent Capabilities
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,7 @@ export default function AgentConfig({
|
||||||
const visibleToolIds = new Set(selectedToolIds);
|
const visibleToolIds = new Set(selectedToolIds);
|
||||||
|
|
||||||
// Check what group parent tools should be shown if any subtool is present
|
// Check what group parent tools should be shown if any subtool is present
|
||||||
Object.entries(allTools).forEach(([toolId, toolObj]) => {
|
Object.entries(allTools ?? {}).forEach(([toolId, toolObj]) => {
|
||||||
if (toolObj.tools?.length) {
|
if (toolObj.tools?.length) {
|
||||||
// if any subtool of this group is selected, ensure group parent tool rendered
|
// if any subtool of this group is selected, ensure group parent tool rendered
|
||||||
if (toolObj.tools.some((st) => selectedToolIds.includes(st.tool_id))) {
|
if (toolObj.tools.some((st) => selectedToolIds.includes(st.tool_id))) {
|
||||||
|
|
@ -300,6 +300,7 @@ export default function AgentConfig({
|
||||||
<div className="mb-1">
|
<div className="mb-1">
|
||||||
{/* // Render all visible IDs (including groups with subtools selected) */}
|
{/* // Render all visible IDs (including groups with subtools selected) */}
|
||||||
{[...visibleToolIds].map((toolId, i) => {
|
{[...visibleToolIds].map((toolId, i) => {
|
||||||
|
if (!allTools) return null;
|
||||||
const tool = allTools[toolId];
|
const tool = allTools[toolId];
|
||||||
if (!tool) return null;
|
if (!tool) return null;
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ export default function AgentTool({
|
||||||
allTools,
|
allTools,
|
||||||
}: {
|
}: {
|
||||||
tool: string;
|
tool: string;
|
||||||
allTools: Record<string, AgentToolType & { tools?: AgentToolType[] }>;
|
allTools?: Record<string, AgentToolType & { tools?: AgentToolType[] }>;
|
||||||
agent_id?: string;
|
agent_id?: string;
|
||||||
}) {
|
}) {
|
||||||
const [isHovering, setIsHovering] = useState(false);
|
const [isHovering, setIsHovering] = useState(false);
|
||||||
|
|
@ -30,8 +30,10 @@ export default function AgentTool({
|
||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
const updateUserPlugins = useUpdateUserPluginsMutation();
|
const updateUserPlugins = useUpdateUserPluginsMutation();
|
||||||
const { getValues, setValue } = useFormContext<AgentForm>();
|
const { getValues, setValue } = useFormContext<AgentForm>();
|
||||||
|
if (!allTools) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const currentTool = allTools[tool];
|
const currentTool = allTools[tool];
|
||||||
|
|
||||||
const getSelectedTools = () => {
|
const getSelectedTools = () => {
|
||||||
if (!currentTool?.tools) return [];
|
if (!currentTool?.tools) return [];
|
||||||
const formTools = getValues('tools') || [];
|
const formTools = getValues('tools') || [];
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@ import {
|
||||||
} from '~/data-provider';
|
} from '~/data-provider';
|
||||||
import { cn, cardStyle, defaultTextProps, removeFocusOutlines } from '~/utils';
|
import { cn, cardStyle, defaultTextProps, removeFocusOutlines } from '~/utils';
|
||||||
import AssistantConversationStarters from './AssistantConversationStarters';
|
import AssistantConversationStarters from './AssistantConversationStarters';
|
||||||
|
import AssistantToolsDialog from '~/components/Tools/AssistantToolsDialog';
|
||||||
import { useAssistantsMapContext, useToastContext } from '~/Providers';
|
import { useAssistantsMapContext, useToastContext } from '~/Providers';
|
||||||
import { useSelectAssistant, useLocalize } from '~/hooks';
|
import { useSelectAssistant, useLocalize } from '~/hooks';
|
||||||
import { ToolSelectDialog } from '~/components/Tools';
|
|
||||||
import AppendDateCheckbox from './AppendDateCheckbox';
|
import AppendDateCheckbox from './AppendDateCheckbox';
|
||||||
import CapabilitiesForm from './CapabilitiesForm';
|
import CapabilitiesForm from './CapabilitiesForm';
|
||||||
import { SelectDropDown } from '~/components/ui';
|
import { SelectDropDown } from '~/components/ui';
|
||||||
|
|
@ -468,11 +468,10 @@ export default function AssistantPanel({
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ToolSelectDialog
|
<AssistantToolsDialog
|
||||||
|
endpoint={endpoint}
|
||||||
isOpen={showToolDialog}
|
isOpen={showToolDialog}
|
||||||
setIsOpen={setShowToolDialog}
|
setIsOpen={setShowToolDialog}
|
||||||
toolsFormKey="functions"
|
|
||||||
endpoint={endpoint}
|
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,15 @@
|
||||||
import { useState } from 'react';
|
|
||||||
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
||||||
import type { NavLink, NavProps } from '~/common';
|
import type { NavLink, NavProps } from '~/common';
|
||||||
import { Accordion, AccordionItem, AccordionContent } from '~/components/ui/Accordion';
|
import { AccordionContent, AccordionItem, TooltipAnchor, Accordion, Button } from '~/components/ui';
|
||||||
import { TooltipAnchor, Button } from '~/components';
|
import { ActivePanelProvider, useActivePanel } from '~/Providers';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
export default function Nav({ links, isCollapsed, resize, defaultActive }: NavProps) {
|
function NavContent({ links, isCollapsed, resize }: Omit<NavProps, 'defaultActive'>) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const [active, _setActive] = useState<string | undefined>(defaultActive);
|
const { active, setActive } = useActivePanel();
|
||||||
const getVariant = (link: NavLink) => (link.id === active ? 'default' : 'ghost');
|
const getVariant = (link: NavLink) => (link.id === active ? 'default' : 'ghost');
|
||||||
|
|
||||||
const setActive = (id: string) => {
|
|
||||||
localStorage.setItem('side:active-panel', id + '');
|
|
||||||
_setActive(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-collapsed={isCollapsed}
|
data-collapsed={isCollapsed}
|
||||||
|
|
@ -105,3 +99,11 @@ export default function Nav({ links, isCollapsed, resize, defaultActive }: NavPr
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function Nav({ links, isCollapsed, resize, defaultActive }: NavProps) {
|
||||||
|
return (
|
||||||
|
<ActivePanelProvider defaultActive={defaultActive}>
|
||||||
|
<NavContent links={links} isCollapsed={isCollapsed} resize={resize} />
|
||||||
|
</ActivePanelProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
254
client/src/components/Tools/AssistantToolsDialog.tsx
Normal file
254
client/src/components/Tools/AssistantToolsDialog.tsx
Normal file
|
|
@ -0,0 +1,254 @@
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { Search, X } from 'lucide-react';
|
||||||
|
import { Dialog, DialogPanel, DialogTitle, Description } from '@headlessui/react';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
import { isAgentsEndpoint } from 'librechat-data-provider';
|
||||||
|
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
|
||||||
|
import type {
|
||||||
|
AssistantsEndpoint,
|
||||||
|
EModelEndpoint,
|
||||||
|
TPluginAction,
|
||||||
|
TError,
|
||||||
|
} from 'librechat-data-provider';
|
||||||
|
import type { TPluginStoreDialogProps } from '~/common/types';
|
||||||
|
import { PluginPagination, PluginAuthForm } from '~/components/Plugins/Store';
|
||||||
|
import { useLocalize, usePluginDialogHelpers } from '~/hooks';
|
||||||
|
import { useAvailableToolsQuery } from '~/data-provider';
|
||||||
|
import ToolItem from './ToolItem';
|
||||||
|
|
||||||
|
function AssistantToolsDialog({
|
||||||
|
isOpen,
|
||||||
|
endpoint,
|
||||||
|
setIsOpen,
|
||||||
|
}: TPluginStoreDialogProps & {
|
||||||
|
endpoint: AssistantsEndpoint | EModelEndpoint.agents;
|
||||||
|
}) {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const { getValues, setValue } = useFormContext();
|
||||||
|
const { data: tools } = useAvailableToolsQuery(endpoint);
|
||||||
|
const isAgentTools = isAgentsEndpoint(endpoint);
|
||||||
|
|
||||||
|
const {
|
||||||
|
maxPage,
|
||||||
|
setMaxPage,
|
||||||
|
currentPage,
|
||||||
|
setCurrentPage,
|
||||||
|
itemsPerPage,
|
||||||
|
searchChanged,
|
||||||
|
setSearchChanged,
|
||||||
|
searchValue,
|
||||||
|
setSearchValue,
|
||||||
|
gridRef,
|
||||||
|
handleSearch,
|
||||||
|
handleChangePage,
|
||||||
|
error,
|
||||||
|
setError,
|
||||||
|
errorMessage,
|
||||||
|
setErrorMessage,
|
||||||
|
showPluginAuthForm,
|
||||||
|
setShowPluginAuthForm,
|
||||||
|
selectedPlugin,
|
||||||
|
setSelectedPlugin,
|
||||||
|
} = usePluginDialogHelpers();
|
||||||
|
|
||||||
|
const updateUserPlugins = useUpdateUserPluginsMutation();
|
||||||
|
const handleInstallError = (error: TError) => {
|
||||||
|
setError(true);
|
||||||
|
const errorMessage = error.response?.data?.message ?? '';
|
||||||
|
if (errorMessage) {
|
||||||
|
setErrorMessage(errorMessage);
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
setError(false);
|
||||||
|
setErrorMessage('');
|
||||||
|
}, 5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInstall = (pluginAction: TPluginAction) => {
|
||||||
|
const addFunction = () => {
|
||||||
|
const fns = getValues('functions').slice();
|
||||||
|
fns.push(pluginAction.pluginKey);
|
||||||
|
setValue('functions', fns);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!pluginAction.auth) {
|
||||||
|
return addFunction();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUserPlugins.mutate(pluginAction, {
|
||||||
|
onError: (error: unknown) => {
|
||||||
|
handleInstallError(error as TError);
|
||||||
|
},
|
||||||
|
onSuccess: addFunction,
|
||||||
|
});
|
||||||
|
|
||||||
|
setShowPluginAuthForm(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemoveTool = (tool: string) => {
|
||||||
|
setShowPluginAuthForm(false);
|
||||||
|
updateUserPlugins.mutate(
|
||||||
|
{ pluginKey: tool, action: 'uninstall', auth: null, isEntityTool: true },
|
||||||
|
{
|
||||||
|
onError: (error: unknown) => {
|
||||||
|
handleInstallError(error as TError);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
const fns = getValues('functions').filter((fn: string) => fn !== tool);
|
||||||
|
setValue('functions', fns);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAddTool = (pluginKey: string) => {
|
||||||
|
setShowPluginAuthForm(false);
|
||||||
|
const getAvailablePluginFromKey = tools?.find((p) => p.pluginKey === pluginKey);
|
||||||
|
setSelectedPlugin(getAvailablePluginFromKey);
|
||||||
|
|
||||||
|
const { authConfig, authenticated = false } = getAvailablePluginFromKey ?? {};
|
||||||
|
|
||||||
|
if (authConfig && authConfig.length > 0 && !authenticated) {
|
||||||
|
setShowPluginAuthForm(true);
|
||||||
|
} else {
|
||||||
|
handleInstall({ pluginKey, action: 'install', auth: null });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredTools = tools?.filter((tool) =>
|
||||||
|
tool.name.toLowerCase().includes(searchValue.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (filteredTools) {
|
||||||
|
setMaxPage(Math.ceil(filteredTools.length / itemsPerPage));
|
||||||
|
if (searchChanged) {
|
||||||
|
setCurrentPage(1);
|
||||||
|
setSearchChanged(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
tools,
|
||||||
|
itemsPerPage,
|
||||||
|
searchValue,
|
||||||
|
filteredTools,
|
||||||
|
searchChanged,
|
||||||
|
setMaxPage,
|
||||||
|
setCurrentPage,
|
||||||
|
setSearchChanged,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={isOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setCurrentPage(1);
|
||||||
|
setSearchValue('');
|
||||||
|
}}
|
||||||
|
className="relative z-[102]"
|
||||||
|
>
|
||||||
|
{/* The backdrop, rendered as a fixed sibling to the panel container */}
|
||||||
|
<div className="fixed inset-0 bg-surface-primary opacity-60 transition-opacity dark:opacity-80" />
|
||||||
|
{/* Full-screen container to center the panel */}
|
||||||
|
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||||
|
<DialogPanel
|
||||||
|
className="relative w-full transform overflow-hidden overflow-y-auto rounded-lg bg-surface-secondary text-left shadow-xl transition-all max-sm:h-full sm:mx-7 sm:my-8 sm:max-w-2xl lg:max-w-5xl xl:max-w-7xl"
|
||||||
|
style={{ minHeight: '610px' }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between border-b-[1px] border-border-medium px-4 pb-4 pt-5 sm:p-6">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="text-center sm:text-left">
|
||||||
|
<DialogTitle className="text-lg font-medium leading-6 text-text-primary">
|
||||||
|
{isAgentTools
|
||||||
|
? localize('com_nav_tool_dialog_agents')
|
||||||
|
: localize('com_nav_tool_dialog')}
|
||||||
|
</DialogTitle>
|
||||||
|
<Description className="text-sm text-text-secondary">
|
||||||
|
{localize('com_nav_tool_dialog_description')}
|
||||||
|
</Description>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="sm:mt-0">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
className="inline-block rounded-full text-text-secondary transition-colors hover:text-text-primary"
|
||||||
|
aria-label="Close dialog"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<X aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{error && (
|
||||||
|
<div
|
||||||
|
className="relative m-4 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
{localize('com_nav_plugin_auth_error')} {errorMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{showPluginAuthForm && (
|
||||||
|
<div className="p-4 sm:p-6 sm:pt-4">
|
||||||
|
<PluginAuthForm
|
||||||
|
plugin={selectedPlugin}
|
||||||
|
onSubmit={(installActionData: TPluginAction) => handleInstall(installActionData)}
|
||||||
|
isEntityTool={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="p-4 sm:p-6 sm:pt-4">
|
||||||
|
<div className="mt-4 flex flex-col gap-4">
|
||||||
|
<div className="flex items-center justify-center space-x-4">
|
||||||
|
<Search className="h-6 w-6 text-text-tertiary" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchValue}
|
||||||
|
onChange={handleSearch}
|
||||||
|
placeholder={localize('com_nav_tool_search')}
|
||||||
|
className="w-64 rounded border border-border-medium bg-transparent px-2 py-1 text-text-primary focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={gridRef}
|
||||||
|
className="grid grid-cols-1 grid-rows-2 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
||||||
|
style={{ minHeight: '410px' }}
|
||||||
|
>
|
||||||
|
{filteredTools &&
|
||||||
|
filteredTools
|
||||||
|
.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage)
|
||||||
|
.map((tool, index) => (
|
||||||
|
<ToolItem
|
||||||
|
key={index}
|
||||||
|
tool={tool}
|
||||||
|
isInstalled={getValues('functions').includes(tool.pluginKey)}
|
||||||
|
onAddTool={() => onAddTool(tool.pluginKey)}
|
||||||
|
onRemoveTool={() => onRemoveTool(tool.pluginKey)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 flex flex-col items-center gap-2 sm:flex-row sm:justify-between">
|
||||||
|
{maxPage > 0 ? (
|
||||||
|
<PluginPagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
maxPage={maxPage}
|
||||||
|
onChangePage={handleChangePage}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{ height: '21px' }}></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogPanel>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AssistantToolsDialog;
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { XCircle, PlusCircleIcon, Wrench } from 'lucide-react';
|
import { XCircle, PlusCircleIcon, Wrench } from 'lucide-react';
|
||||||
import { AgentToolType } from 'librechat-data-provider';
|
import type { TPlugin, AgentToolType } from 'librechat-data-provider';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
type ToolItemProps = {
|
type ToolItemProps = {
|
||||||
tool: AgentToolType;
|
tool: TPlugin | AgentToolType;
|
||||||
onAddTool: () => void;
|
onAddTool: () => void;
|
||||||
onRemoveTool: () => void;
|
onRemoveTool: () => void;
|
||||||
isInstalled?: boolean;
|
isInstalled?: boolean;
|
||||||
|
|
@ -19,9 +19,13 @@ function ToolItem({ tool, onAddTool, onRemoveTool, isInstalled = false }: ToolIt
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const name = tool.metadata?.name || tool.tool_id;
|
const name =
|
||||||
const description = tool.metadata?.description || '';
|
(tool as AgentToolType).metadata?.name ||
|
||||||
const icon = tool.metadata?.icon;
|
(tool as AgentToolType).tool_id ||
|
||||||
|
(tool as TPlugin).name;
|
||||||
|
const description =
|
||||||
|
(tool as AgentToolType).metadata?.description || (tool as TPlugin).description || '';
|
||||||
|
const icon = (tool as AgentToolType).metadata?.icon || (tool as TPlugin).icon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 rounded border border-border-medium bg-transparent p-6">
|
<div className="flex flex-col gap-4 rounded border border-border-medium bg-transparent p-6">
|
||||||
|
|
|
||||||
|
|
@ -67,15 +67,14 @@ function ToolSelectDialog({
|
||||||
}, 5000);
|
}, 5000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toolsFormKey = 'tools';
|
|
||||||
const handleInstall = (pluginAction: TPluginAction) => {
|
const handleInstall = (pluginAction: TPluginAction) => {
|
||||||
const addFunction = () => {
|
const addFunction = () => {
|
||||||
const installedToolIds: string[] = getValues(toolsFormKey) || [];
|
const installedToolIds: string[] = getValues('tools') || [];
|
||||||
// Add the parent
|
// Add the parent
|
||||||
installedToolIds.push(pluginAction.pluginKey);
|
installedToolIds.push(pluginAction.pluginKey);
|
||||||
|
|
||||||
// If this tool is a group, add subtools too
|
// If this tool is a group, add subtools too
|
||||||
const groupObj = groupedTools[pluginAction.pluginKey];
|
const groupObj = groupedTools?.[pluginAction.pluginKey];
|
||||||
if (groupObj?.tools && groupObj.tools.length > 0) {
|
if (groupObj?.tools && groupObj.tools.length > 0) {
|
||||||
for (const sub of groupObj.tools) {
|
for (const sub of groupObj.tools) {
|
||||||
if (!installedToolIds.includes(sub.tool_id)) {
|
if (!installedToolIds.includes(sub.tool_id)) {
|
||||||
|
|
@ -83,7 +82,7 @@ function ToolSelectDialog({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setValue(toolsFormKey, Array.from(new Set(installedToolIds))); // no duplicates just in case
|
setValue('tools', Array.from(new Set(installedToolIds))); // no duplicates just in case
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!pluginAction.auth) {
|
if (!pluginAction.auth) {
|
||||||
|
|
@ -101,7 +100,7 @@ function ToolSelectDialog({
|
||||||
};
|
};
|
||||||
|
|
||||||
const onRemoveTool = (toolId: string) => {
|
const onRemoveTool = (toolId: string) => {
|
||||||
const groupObj = groupedTools[toolId];
|
const groupObj = groupedTools?.[toolId];
|
||||||
const toolIdsToRemove = [toolId];
|
const toolIdsToRemove = [toolId];
|
||||||
if (groupObj?.tools && groupObj.tools.length > 0) {
|
if (groupObj?.tools && groupObj.tools.length > 0) {
|
||||||
toolIdsToRemove.push(...groupObj.tools.map((sub) => sub.tool_id));
|
toolIdsToRemove.push(...groupObj.tools.map((sub) => sub.tool_id));
|
||||||
|
|
@ -113,8 +112,8 @@ function ToolSelectDialog({
|
||||||
onError: (error: unknown) => handleInstallError(error as TError),
|
onError: (error: unknown) => handleInstallError(error as TError),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
const remainingToolIds =
|
const remainingToolIds =
|
||||||
getValues(toolsFormKey)?.filter((toolId) => !toolIdsToRemove.includes(toolId)) || [];
|
getValues('tools')?.filter((toolId) => !toolIdsToRemove.includes(toolId)) || [];
|
||||||
setValue(toolsFormKey, remainingToolIds);
|
setValue('tools', remainingToolIds);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -268,7 +267,7 @@ function ToolSelectDialog({
|
||||||
<ToolItem
|
<ToolItem
|
||||||
key={index}
|
key={index}
|
||||||
tool={tool}
|
tool={tool}
|
||||||
isInstalled={getValues(toolsFormKey)?.includes(tool.tool_id) || false}
|
isInstalled={getValues('tools')?.includes(tool.tool_id) || false}
|
||||||
onAddTool={() => onAddTool(tool.tool_id)}
|
onAddTool={() => onAddTool(tool.tool_id)}
|
||||||
onRemoveTool={() => onRemoveTool(tool.tool_id)}
|
onRemoveTool={() => onRemoveTool(tool.tool_id)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
export * from './Accordion';
|
||||||
export * from './AnimatedTabs';
|
export * from './AnimatedTabs';
|
||||||
export * from './AlertDialog';
|
export * from './AlertDialog';
|
||||||
export * from './Breadcrumb';
|
export * from './Breadcrumb';
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,34 @@
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
|
||||||
import type { TEndpointsConfig, TError } from 'librechat-data-provider';
|
|
||||||
import {
|
|
||||||
defaultAssistantsVersion,
|
|
||||||
fileConfig as defaultFileConfig,
|
|
||||||
EModelEndpoint,
|
|
||||||
isAgentsEndpoint,
|
|
||||||
isAssistantsEndpoint,
|
|
||||||
mergeFileConfig,
|
|
||||||
QueryKeys,
|
|
||||||
} from 'librechat-data-provider';
|
|
||||||
import debounce from 'lodash/debounce';
|
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import {
|
||||||
|
QueryKeys,
|
||||||
|
EModelEndpoint,
|
||||||
|
mergeFileConfig,
|
||||||
|
isAgentsEndpoint,
|
||||||
|
isAssistantsEndpoint,
|
||||||
|
defaultAssistantsVersion,
|
||||||
|
fileConfig as defaultFileConfig,
|
||||||
|
} from 'librechat-data-provider';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import type { EndpointFileConfig, TEndpointsConfig, TError } from 'librechat-data-provider';
|
||||||
import type { ExtendedFile, FileSetter } from '~/common';
|
import type { ExtendedFile, FileSetter } from '~/common';
|
||||||
import { useGetFileConfig, useUploadFileMutation } from '~/data-provider';
|
import { useGetFileConfig, useUploadFileMutation } from '~/data-provider';
|
||||||
import useLocalize, { TranslationKeys } from '~/hooks/useLocalize';
|
import useLocalize, { TranslationKeys } from '~/hooks/useLocalize';
|
||||||
import { useChatContext } from '~/Providers/ChatContext';
|
import { useDelayedUploadToast } from './useDelayedUploadToast';
|
||||||
|
import { processFileForUpload } from '~/utils/heicConverter';
|
||||||
import { useToastContext } from '~/Providers/ToastContext';
|
import { useToastContext } from '~/Providers/ToastContext';
|
||||||
|
import { useChatContext } from '~/Providers/ChatContext';
|
||||||
import { logger, validateFiles } from '~/utils';
|
import { logger, validateFiles } from '~/utils';
|
||||||
import useClientResize from './useClientResize';
|
import useClientResize from './useClientResize';
|
||||||
import { processFileForUpload } from '~/utils/heicConverter';
|
|
||||||
import { useDelayedUploadToast } from './useDelayedUploadToast';
|
|
||||||
import useUpdateFiles from './useUpdateFiles';
|
import useUpdateFiles from './useUpdateFiles';
|
||||||
|
|
||||||
type UseFileHandling = {
|
type UseFileHandling = {
|
||||||
overrideEndpoint?: EModelEndpoint;
|
|
||||||
fileSetter?: FileSetter;
|
fileSetter?: FileSetter;
|
||||||
fileFilter?: (file: File) => boolean;
|
fileFilter?: (file: File) => boolean;
|
||||||
additionalMetadata?: Record<string, string | undefined>;
|
additionalMetadata?: Record<string, string | undefined>;
|
||||||
|
overrideEndpoint?: EModelEndpoint;
|
||||||
|
overrideEndpointFileConfig?: EndpointFileConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useFileHandling = (params?: UseFileHandling) => {
|
const useFileHandling = (params?: UseFileHandling) => {
|
||||||
|
|
@ -246,8 +247,9 @@ const useFileHandling = (params?: UseFileHandling) => {
|
||||||
fileList,
|
fileList,
|
||||||
setError,
|
setError,
|
||||||
endpointFileConfig:
|
endpointFileConfig:
|
||||||
fileConfig?.endpoints[endpoint] ??
|
params?.overrideEndpointFileConfig ??
|
||||||
fileConfig?.endpoints.default ??
|
fileConfig?.endpoints?.[endpoint] ??
|
||||||
|
fileConfig?.endpoints?.default ??
|
||||||
defaultFileConfig.endpoints[endpoint] ??
|
defaultFileConfig.endpoints[endpoint] ??
|
||||||
defaultFileConfig.endpoints.default,
|
defaultFileConfig.endpoints.default,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ export default function useSideNavLinks({
|
||||||
title: 'com_sidepanel_assistant_builder',
|
title: 'com_sidepanel_assistant_builder',
|
||||||
label: '',
|
label: '',
|
||||||
icon: Blocks,
|
icon: Blocks,
|
||||||
id: 'assistants',
|
id: EModelEndpoint.assistants,
|
||||||
Component: PanelSwitch,
|
Component: PanelSwitch,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -92,7 +92,7 @@ export default function useSideNavLinks({
|
||||||
title: 'com_sidepanel_agent_builder',
|
title: 'com_sidepanel_agent_builder',
|
||||||
label: '',
|
label: '',
|
||||||
icon: Blocks,
|
icon: Blocks,
|
||||||
id: 'agents',
|
id: EModelEndpoint.agents,
|
||||||
Component: AgentPanelSwitch,
|
Component: AgentPanelSwitch,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ export * from './oauth';
|
||||||
export * from './crypto';
|
export * from './crypto';
|
||||||
/* Flow */
|
/* Flow */
|
||||||
export * from './flow/manager';
|
export * from './flow/manager';
|
||||||
|
/* Middleware */
|
||||||
|
export * from './middleware';
|
||||||
/* Agents */
|
/* Agents */
|
||||||
export * from './agents';
|
export * from './agents';
|
||||||
/* Endpoints */
|
/* Endpoints */
|
||||||
|
|
|
||||||
141
packages/api/src/middleware/access.ts
Normal file
141
packages/api/src/middleware/access.ts
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
import { logger } from '@librechat/data-schemas';
|
||||||
|
import {
|
||||||
|
Permissions,
|
||||||
|
EndpointURLs,
|
||||||
|
EModelEndpoint,
|
||||||
|
PermissionTypes,
|
||||||
|
isAgentsEndpoint,
|
||||||
|
} from 'librechat-data-provider';
|
||||||
|
import type { NextFunction, Request as ServerRequest, Response as ServerResponse } from 'express';
|
||||||
|
import type { IRole, IUser } from '@librechat/data-schemas';
|
||||||
|
|
||||||
|
export function skipAgentCheck(req?: ServerRequest): boolean {
|
||||||
|
if (!req || !req?.body?.endpoint) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method !== 'POST') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.originalUrl?.includes(EndpointURLs[EModelEndpoint.agents])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !isAgentsEndpoint(req.body.endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core function to check if a user has one or more required permissions
|
||||||
|
* @param user - The user object
|
||||||
|
* @param permissionType - The type of permission to check
|
||||||
|
* @param permissions - The list of specific permissions to check
|
||||||
|
* @param bodyProps - An optional object where keys are permissions and values are arrays of properties to check
|
||||||
|
* @param checkObject - The object to check properties against
|
||||||
|
* @param skipCheck - An optional function that takes the checkObject and returns true to skip permission checking
|
||||||
|
* @returns Whether the user has the required permissions
|
||||||
|
*/
|
||||||
|
export const checkAccess = async ({
|
||||||
|
req,
|
||||||
|
user,
|
||||||
|
permissionType,
|
||||||
|
permissions,
|
||||||
|
getRoleByName,
|
||||||
|
bodyProps = {} as Record<Permissions, string[]>,
|
||||||
|
checkObject = {},
|
||||||
|
skipCheck,
|
||||||
|
}: {
|
||||||
|
user: IUser;
|
||||||
|
req?: ServerRequest;
|
||||||
|
permissionType: PermissionTypes;
|
||||||
|
permissions: Permissions[];
|
||||||
|
bodyProps?: Record<Permissions, string[]>;
|
||||||
|
checkObject?: object;
|
||||||
|
/** If skipCheck function is provided and returns true, skip permission checking */
|
||||||
|
skipCheck?: (req?: ServerRequest) => boolean;
|
||||||
|
getRoleByName: (roleName: string, fieldsToSelect?: string | string[]) => Promise<IRole | null>;
|
||||||
|
}): Promise<boolean> => {
|
||||||
|
if (skipCheck && skipCheck(req)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user || !user.role) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const role = await getRoleByName(user.role);
|
||||||
|
if (role && role.permissions && role.permissions[permissionType]) {
|
||||||
|
const hasAnyPermission = permissions.some((permission) => {
|
||||||
|
if (
|
||||||
|
role.permissions?.[permissionType as keyof typeof role.permissions]?.[
|
||||||
|
permission as keyof (typeof role.permissions)[typeof permissionType]
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bodyProps[permission] && checkObject) {
|
||||||
|
return bodyProps[permission].some((prop) =>
|
||||||
|
Object.prototype.hasOwnProperty.call(checkObject, prop),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return hasAnyPermission;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware to check if a user has one or more required permissions, optionally based on `req.body` properties.
|
||||||
|
* @param permissionType - The type of permission to check.
|
||||||
|
* @param permissions - The list of specific permissions to check.
|
||||||
|
* @param bodyProps - An optional object where keys are permissions and values are arrays of `req.body` properties to check.
|
||||||
|
* @param skipCheck - An optional function that takes req.body and returns true to skip permission checking.
|
||||||
|
* @param getRoleByName - A function to get the role by name.
|
||||||
|
* @returns Express middleware function.
|
||||||
|
*/
|
||||||
|
export const generateCheckAccess = ({
|
||||||
|
permissionType,
|
||||||
|
permissions,
|
||||||
|
bodyProps = {} as Record<Permissions, string[]>,
|
||||||
|
skipCheck,
|
||||||
|
getRoleByName,
|
||||||
|
}: {
|
||||||
|
permissionType: PermissionTypes;
|
||||||
|
permissions: Permissions[];
|
||||||
|
bodyProps?: Record<Permissions, string[]>;
|
||||||
|
skipCheck?: (req?: ServerRequest) => boolean;
|
||||||
|
getRoleByName: (roleName: string, fieldsToSelect?: string | string[]) => Promise<IRole | null>;
|
||||||
|
}): ((req: ServerRequest, res: ServerResponse, next: NextFunction) => Promise<unknown>) => {
|
||||||
|
return async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const hasAccess = await checkAccess({
|
||||||
|
req,
|
||||||
|
user: req.user as IUser,
|
||||||
|
permissionType,
|
||||||
|
permissions,
|
||||||
|
bodyProps,
|
||||||
|
checkObject: req.body,
|
||||||
|
skipCheck,
|
||||||
|
getRoleByName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasAccess) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn(
|
||||||
|
`[${permissionType}] Forbidden: "${req.originalUrl}" - Insufficient permissions for User ${req.user?.id}: ${permissions.join(', ')}`,
|
||||||
|
);
|
||||||
|
return res.status(403).json({ message: 'Forbidden: Insufficient permissions' });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return res.status(500).json({
|
||||||
|
message: `Server error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
1
packages/api/src/middleware/index.ts
Normal file
1
packages/api/src/middleware/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './access';
|
||||||
|
|
@ -949,11 +949,11 @@ export const initialModelsConfig: TModelsConfig = {
|
||||||
[EModelEndpoint.bedrock]: defaultModels[EModelEndpoint.bedrock],
|
[EModelEndpoint.bedrock]: defaultModels[EModelEndpoint.bedrock],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EndpointURLs: Record<string, string> = {
|
export const EndpointURLs = {
|
||||||
[EModelEndpoint.assistants]: '/api/assistants/v2/chat',
|
[EModelEndpoint.assistants]: '/api/assistants/v2/chat',
|
||||||
[EModelEndpoint.azureAssistants]: '/api/assistants/v1/chat',
|
[EModelEndpoint.azureAssistants]: '/api/assistants/v1/chat',
|
||||||
[EModelEndpoint.agents]: `/api/${EModelEndpoint.agents}/chat`,
|
[EModelEndpoint.agents]: `/api/${EModelEndpoint.agents}/chat`,
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
export const modularEndpoints = new Set<EModelEndpoint | string>([
|
export const modularEndpoints = new Set<EModelEndpoint | string>([
|
||||||
EModelEndpoint.gptPlugins,
|
EModelEndpoint.gptPlugins,
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,7 @@ export type EventSubmission = Omit<TSubmission, 'initialResponse'> & { initialRe
|
||||||
export type TPluginAction = {
|
export type TPluginAction = {
|
||||||
pluginKey: string;
|
pluginKey: string;
|
||||||
action: 'install' | 'uninstall';
|
action: 'install' | 'uninstall';
|
||||||
auth?: Partial<Record<string, string>>;
|
auth?: Partial<Record<string, string>> | null;
|
||||||
isEntityTool?: boolean;
|
isEntityTool?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -144,7 +144,7 @@ export type TUpdateUserPlugins = {
|
||||||
isEntityTool?: boolean;
|
isEntityTool?: boolean;
|
||||||
pluginKey: string;
|
pluginKey: string;
|
||||||
action: string;
|
action: string;
|
||||||
auth?: Partial<Record<string, string | null>>;
|
auth?: Partial<Record<string, string | null>> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO `label` needs to be changed to the proper `TranslationKeys`
|
// TODO `label` needs to be changed to the proper `TranslationKeys`
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue