mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +01:00
* feat: Add 'Run Code' and 'Temporary Chat' permissions to role management * feat: Add NextFunction typedef to api/typedefs.js * feat: Add temporary chat and run code permissions to role schema * refactor: Enhance access check middleware with logging for permission errors and better typing * refactor: Set default value of USE permission to true in multiConvoPermissionsSchema * refactor: Implement checkAccess function for separation of permission validation logic from middleware * feat: Integrate permission checks for tool execution and enhance Markdown code block with execution capability * fix: Convert REDIS_MAX_LISTENERS to a number, closes #5979
207 lines
6.3 KiB
JavaScript
207 lines
6.3 KiB
JavaScript
const { nanoid } = require('nanoid');
|
|
const { EnvVar } = require('@librechat/agents');
|
|
const {
|
|
Tools,
|
|
AuthType,
|
|
Permissions,
|
|
ToolCallTypes,
|
|
PermissionTypes,
|
|
} = require('librechat-data-provider');
|
|
const { processFileURL, uploadImageBuffer } = require('~/server/services/Files/process');
|
|
const { processCodeOutput } = require('~/server/services/Files/Code/process');
|
|
const { createToolCall, getToolCallsByConvo } = require('~/models/ToolCall');
|
|
const { loadAuthValues, loadTools } = require('~/app/clients/tools/util');
|
|
const { checkAccess } = require('~/server/middleware');
|
|
const { getMessage } = require('~/models/Message');
|
|
const { logger } = require('~/config');
|
|
|
|
const fieldsMap = {
|
|
[Tools.execute_code]: [EnvVar.CODE_API_KEY],
|
|
};
|
|
|
|
const toolAccessPermType = {
|
|
[Tools.execute_code]: PermissionTypes.RUN_CODE,
|
|
};
|
|
|
|
/**
|
|
* @param {ServerRequest} req - The request object, containing information about the HTTP request.
|
|
* @param {ServerResponse} res - The response object, used to send back the desired HTTP response.
|
|
* @returns {Promise<void>} A promise that resolves when the function has completed.
|
|
*/
|
|
const verifyToolAuth = async (req, res) => {
|
|
try {
|
|
const { toolId } = req.params;
|
|
const authFields = fieldsMap[toolId];
|
|
if (!authFields) {
|
|
res.status(404).json({ message: 'Tool not found' });
|
|
return;
|
|
}
|
|
let result;
|
|
try {
|
|
result = await loadAuthValues({
|
|
userId: req.user.id,
|
|
authFields,
|
|
throwError: false,
|
|
});
|
|
} catch (error) {
|
|
res.status(200).json({ authenticated: false, message: AuthType.USER_PROVIDED });
|
|
return;
|
|
}
|
|
let isUserProvided = false;
|
|
for (const field of authFields) {
|
|
if (!result[field]) {
|
|
res.status(200).json({ authenticated: false, message: AuthType.USER_PROVIDED });
|
|
return;
|
|
}
|
|
if (!isUserProvided && process.env[field] !== result[field]) {
|
|
isUserProvided = true;
|
|
}
|
|
}
|
|
res.status(200).json({
|
|
authenticated: true,
|
|
message: isUserProvided ? AuthType.USER_PROVIDED : AuthType.SYSTEM_DEFINED,
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({ message: error.message });
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @param {ServerRequest} req - The request object, containing information about the HTTP request.
|
|
* @param {ServerResponse} res - The response object, used to send back the desired HTTP response.
|
|
* @param {NextFunction} next - The next middleware function to call.
|
|
* @returns {Promise<void>} A promise that resolves when the function has completed.
|
|
*/
|
|
const callTool = async (req, res) => {
|
|
try {
|
|
const { toolId = '' } = req.params;
|
|
if (!fieldsMap[toolId]) {
|
|
logger.warn(`[${toolId}/call] User ${req.user.id} attempted call to invalid tool`);
|
|
res.status(404).json({ message: 'Tool not found' });
|
|
return;
|
|
}
|
|
|
|
const { partIndex, blockIndex, messageId, conversationId, ...args } = req.body;
|
|
if (!messageId) {
|
|
logger.warn(`[${toolId}/call] User ${req.user.id} attempted call without message ID`);
|
|
res.status(400).json({ message: 'Message ID required' });
|
|
return;
|
|
}
|
|
|
|
const message = await getMessage({ user: req.user.id, messageId });
|
|
if (!message) {
|
|
logger.debug(`[${toolId}/call] User ${req.user.id} attempted call with invalid message ID`);
|
|
res.status(404).json({ message: 'Message not found' });
|
|
return;
|
|
}
|
|
logger.debug(`[${toolId}/call] User: ${req.user.id}`);
|
|
let hasAccess = true;
|
|
if (toolAccessPermType[toolId]) {
|
|
hasAccess = await checkAccess(req.user, toolAccessPermType[toolId], [Permissions.USE]);
|
|
}
|
|
if (!hasAccess) {
|
|
logger.warn(
|
|
`[${toolAccessPermType[toolId]}] Forbidden: Insufficient permissions for User ${req.user.id}: ${Permissions.USE}`,
|
|
);
|
|
return res.status(403).json({ message: 'Forbidden: Insufficient permissions' });
|
|
}
|
|
const { loadedTools } = await loadTools({
|
|
user: req.user.id,
|
|
tools: [toolId],
|
|
functions: true,
|
|
options: {
|
|
req,
|
|
returnMetadata: true,
|
|
processFileURL,
|
|
uploadImageBuffer,
|
|
fileStrategy: req.app.locals.fileStrategy,
|
|
},
|
|
});
|
|
|
|
const tool = loadedTools[0];
|
|
const toolCallId = `${req.user.id}_${nanoid()}`;
|
|
const result = await tool.invoke({
|
|
args,
|
|
name: toolId,
|
|
id: toolCallId,
|
|
type: ToolCallTypes.TOOL_CALL,
|
|
});
|
|
|
|
const { content, artifact } = result;
|
|
const toolCallData = {
|
|
toolId,
|
|
messageId,
|
|
partIndex,
|
|
blockIndex,
|
|
conversationId,
|
|
result: content,
|
|
user: req.user.id,
|
|
};
|
|
|
|
if (!artifact || !artifact.files || toolId !== Tools.execute_code) {
|
|
createToolCall(toolCallData).catch((error) => {
|
|
logger.error(`Error creating tool call: ${error.message}`);
|
|
});
|
|
return res.status(200).json({
|
|
result: content,
|
|
});
|
|
}
|
|
|
|
const artifactPromises = [];
|
|
for (const file of artifact.files) {
|
|
const { id, name } = file;
|
|
artifactPromises.push(
|
|
(async () => {
|
|
const fileMetadata = await processCodeOutput({
|
|
req,
|
|
id,
|
|
name,
|
|
apiKey: tool.apiKey,
|
|
messageId,
|
|
toolCallId,
|
|
conversationId,
|
|
session_id: artifact.session_id,
|
|
});
|
|
|
|
if (!fileMetadata) {
|
|
return null;
|
|
}
|
|
|
|
return fileMetadata;
|
|
})().catch((error) => {
|
|
logger.error('Error processing code output:', error);
|
|
return null;
|
|
}),
|
|
);
|
|
}
|
|
const attachments = await Promise.all(artifactPromises);
|
|
toolCallData.attachments = attachments;
|
|
createToolCall(toolCallData).catch((error) => {
|
|
logger.error(`Error creating tool call: ${error.message}`);
|
|
});
|
|
res.status(200).json({
|
|
result: content,
|
|
attachments,
|
|
});
|
|
} catch (error) {
|
|
logger.error('Error calling tool', error);
|
|
res.status(500).json({ message: 'Error calling tool' });
|
|
}
|
|
};
|
|
|
|
const getToolCalls = async (req, res) => {
|
|
try {
|
|
const { conversationId } = req.query;
|
|
const toolCalls = await getToolCallsByConvo(conversationId, req.user.id);
|
|
res.status(200).json(toolCalls);
|
|
} catch (error) {
|
|
logger.error('Error getting tool calls', error);
|
|
res.status(500).json({ message: 'Error getting tool calls' });
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
callTool,
|
|
getToolCalls,
|
|
verifyToolAuth,
|
|
};
|