🔼 feat: "Run Code" Button Toggle (#5988)

* 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
This commit is contained in:
Danny Avila 2025-02-23 14:01:36 -05:00 committed by GitHub
parent 2a74ceb630
commit 0e719592c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 198 additions and 34 deletions

View file

@ -9,7 +9,7 @@ const { REDIS_URI, USE_REDIS, USE_REDIS_CLUSTER, REDIS_CA, REDIS_KEY_PREFIX, RED
let keyvRedis;
const redis_prefix = REDIS_KEY_PREFIX || '';
const redis_max_listeners = REDIS_MAX_LISTENERS || 10;
const redis_max_listeners = Number(REDIS_MAX_LISTENERS) || 10;
function mapURI(uri) {
const regex =

View file

@ -6,8 +6,10 @@ const {
removeNullishValues,
agentPermissionsSchema,
promptPermissionsSchema,
runCodePermissionsSchema,
bookmarkPermissionsSchema,
multiConvoPermissionsSchema,
temporaryChatPermissionsSchema,
} = require('librechat-data-provider');
const getLogStores = require('~/cache/getLogStores');
const Role = require('~/models/schema/roleSchema');
@ -77,6 +79,8 @@ const permissionSchemas = {
[PermissionTypes.PROMPTS]: promptPermissionsSchema,
[PermissionTypes.BOOKMARKS]: bookmarkPermissionsSchema,
[PermissionTypes.MULTI_CONVO]: multiConvoPermissionsSchema,
[PermissionTypes.TEMPORARY_CHAT]: temporaryChatPermissionsSchema,
[PermissionTypes.RUN_CODE]: runCodePermissionsSchema,
};
/**

View file

@ -48,6 +48,18 @@ const roleSchema = new mongoose.Schema({
default: true,
},
},
[PermissionTypes.TEMPORARY_CHAT]: {
[Permissions.USE]: {
type: Boolean,
default: true,
},
},
[PermissionTypes.RUN_CODE]: {
[Permissions.USE]: {
type: Boolean,
default: true,
},
},
});
const Role = mongoose.model('Role', roleSchema);

View file

@ -1,10 +1,17 @@
const { nanoid } = require('nanoid');
const { EnvVar } = require('@librechat/agents');
const { Tools, AuthType, ToolCallTypes } = require('librechat-data-provider');
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 { loadAuthValues, loadTools } = require('~/app/clients/tools/util');
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');
@ -12,6 +19,10 @@ 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.
@ -58,6 +69,7 @@ const verifyToolAuth = async (req, res) => {
/**
* @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) => {
@ -83,6 +95,16 @@ const callTool = async (req, res) => {
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],

View file

@ -1,19 +1,19 @@
const { getRoleByName } = require('~/models/Role');
const { logger } = require('~/config');
/**
* Middleware to check if a user has one or more required permissions, optionally based on `req.body` properties.
* Core function to check if a user has one or more required permissions
*
* @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 {Function} Express middleware function.
* @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 generateCheckAccess = (permissionType, permissions, bodyProps = {}) => {
return async (req, res, next) => {
try {
const { user } = req;
const checkAccess = async (user, permissionType, permissions, bodyProps = {}, checkObject = {}) => {
if (!user) {
return res.status(401).json({ message: 'Authorization required' });
return false;
}
const role = await getRoleByName(user.role);
@ -23,25 +23,56 @@ const generateCheckAccess = (permissionType, permissions, bodyProps = {}) => {
return true;
}
if (bodyProps[permission] && req.body) {
if (bodyProps[permission] && checkObject) {
return bodyProps[permission].some((prop) =>
Object.prototype.hasOwnProperty.call(req.body, prop),
Object.prototype.hasOwnProperty.call(checkObject, prop),
);
}
return false;
});
if (hasAnyPermission) {
return next();
}
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 = generateCheckAccess;
module.exports = {
checkAccess,
generateCheckAccess,
};

View file

@ -1,7 +1,8 @@
const checkAdmin = require('./checkAdmin');
const generateCheckAccess = require('./generateCheckAccess');
const { checkAccess, generateCheckAccess } = require('./generateCheckAccess');
module.exports = {
checkAdmin,
checkAccess,
generateCheckAccess,
};

View file

@ -34,6 +34,7 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol
multiConvo: interfaceConfig?.multiConvo ?? defaults.multiConvo,
agents: interfaceConfig?.agents ?? defaults.agents,
temporaryChat: interfaceConfig?.temporaryChat ?? defaults.temporaryChat,
runCode: interfaceConfig?.runCode ?? defaults.runCode,
customWelcome: interfaceConfig?.customWelcome ?? defaults.customWelcome,
});
@ -42,12 +43,16 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: loadedInterface.bookmarks },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: loadedInterface.multiConvo },
[PermissionTypes.AGENTS]: { [Permissions.USE]: loadedInterface.agents },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: loadedInterface.temporaryChat },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: loadedInterface.runCode },
});
await updateAccessPermissions(SystemRoles.ADMIN, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: loadedInterface.prompts },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: loadedInterface.bookmarks },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: loadedInterface.multiConvo },
[PermissionTypes.AGENTS]: { [Permissions.USE]: loadedInterface.agents },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: loadedInterface.temporaryChat },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: loadedInterface.runCode },
});
let i = 0;

View file

@ -14,6 +14,8 @@ describe('loadDefaultInterface', () => {
bookmarks: true,
multiConvo: true,
agents: true,
temporaryChat: true,
runCode: true,
},
};
const configDefaults = { interface: {} };
@ -25,6 +27,8 @@ describe('loadDefaultInterface', () => {
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
[PermissionTypes.AGENTS]: { [Permissions.USE]: true },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: true },
});
});
@ -35,6 +39,8 @@ describe('loadDefaultInterface', () => {
bookmarks: false,
multiConvo: false,
agents: false,
temporaryChat: false,
runCode: false,
},
};
const configDefaults = { interface: {} };
@ -46,6 +52,8 @@ describe('loadDefaultInterface', () => {
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: false },
[PermissionTypes.AGENTS]: { [Permissions.USE]: false },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: false },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: false },
});
});
@ -60,6 +68,8 @@ describe('loadDefaultInterface', () => {
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
[PermissionTypes.AGENTS]: { [Permissions.USE]: undefined },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined },
});
});
@ -70,6 +80,8 @@ describe('loadDefaultInterface', () => {
bookmarks: undefined,
multiConvo: undefined,
agents: undefined,
temporaryChat: undefined,
runCode: undefined,
},
};
const configDefaults = { interface: {} };
@ -81,6 +93,8 @@ describe('loadDefaultInterface', () => {
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
[PermissionTypes.AGENTS]: { [Permissions.USE]: undefined },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined },
});
});
@ -91,6 +105,8 @@ describe('loadDefaultInterface', () => {
bookmarks: false,
multiConvo: undefined,
agents: true,
temporaryChat: undefined,
runCode: false,
},
};
const configDefaults = { interface: {} };
@ -102,6 +118,8 @@ describe('loadDefaultInterface', () => {
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
[PermissionTypes.AGENTS]: { [Permissions.USE]: true },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: false },
});
});
@ -113,6 +131,8 @@ describe('loadDefaultInterface', () => {
bookmarks: true,
multiConvo: true,
agents: true,
temporaryChat: true,
runCode: true,
},
};
@ -123,6 +143,8 @@ describe('loadDefaultInterface', () => {
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
[PermissionTypes.AGENTS]: { [Permissions.USE]: true },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: true },
});
});
@ -137,6 +159,8 @@ describe('loadDefaultInterface', () => {
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
[PermissionTypes.AGENTS]: { [Permissions.USE]: undefined },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined },
});
});
@ -151,6 +175,8 @@ describe('loadDefaultInterface', () => {
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: false },
[PermissionTypes.AGENTS]: { [Permissions.USE]: undefined },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined },
});
});
@ -165,6 +191,8 @@ describe('loadDefaultInterface', () => {
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
[PermissionTypes.AGENTS]: { [Permissions.USE]: undefined },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined },
});
});
@ -175,6 +203,8 @@ describe('loadDefaultInterface', () => {
bookmarks: false,
multiConvo: true,
agents: false,
temporaryChat: true,
runCode: false,
},
};
const configDefaults = { interface: {} };
@ -186,6 +216,8 @@ describe('loadDefaultInterface', () => {
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
[PermissionTypes.AGENTS]: { [Permissions.USE]: false },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: false },
});
});
@ -197,6 +229,8 @@ describe('loadDefaultInterface', () => {
bookmarks: true,
multiConvo: false,
agents: undefined,
temporaryChat: undefined,
runCode: undefined,
},
};
@ -207,6 +241,8 @@ describe('loadDefaultInterface', () => {
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: false },
[PermissionTypes.AGENTS]: { [Permissions.USE]: undefined },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined },
});
});
});

View file

@ -20,6 +20,12 @@
* @memberof typedefs
*/
/**
* @exports NextFunction
* @typedef {import('express').NextFunction} NextFunction
* @memberof typedefs
*/
/**
* @exports AgentRun
* @typedef {import('@librechat/agents').Run} AgentRun

View file

@ -7,6 +7,7 @@ import { useRecoilValue } from 'recoil';
import ReactMarkdown from 'react-markdown';
import rehypeHighlight from 'rehype-highlight';
import remarkDirective from 'remark-directive';
import { PermissionTypes, Permissions } from 'librechat-data-provider';
import type { Pluggable } from 'unified';
import {
useToastContext,
@ -17,6 +18,7 @@ import {
import { Artifact, artifactPlugin } from '~/components/Artifacts/Artifact';
import { langSubset, preprocessLaTeX, handleDoubleClick } from '~/utils';
import CodeBlock from '~/components/Messages/Content/CodeBlock';
import useHasAccess from '~/hooks/Roles/useHasAccess';
import { useFileDownload } from '~/data-provider';
import useLocalize from '~/hooks/useLocalize';
import store from '~/store';
@ -28,6 +30,10 @@ type TCodeProps = {
};
export const code: React.ElementType = memo(({ className, children }: TCodeProps) => {
const canRunCode = useHasAccess({
permissionType: PermissionTypes.RUN_CODE,
permission: Permissions.USE,
});
const match = /language-(\w+)/.exec(className ?? '');
const lang = match && match[1];
const isMath = lang === 'math';
@ -49,7 +55,14 @@ export const code: React.ElementType = memo(({ className, children }: TCodeProps
</code>
);
} else {
return <CodeBlock lang={lang ?? 'text'} codeChildren={children} blockIndex={blockIndex} />;
return (
<CodeBlock
lang={lang ?? 'text'}
codeChildren={children}
blockIndex={blockIndex}
allowExecution={canRunCode}
/>
);
}
});

2
package-lock.json generated
View file

@ -38885,7 +38885,7 @@
},
"packages/data-provider": {
"name": "librechat-data-provider",
"version": "0.7.6991",
"version": "0.7.6992",
"license": "ISC",
"dependencies": {
"axios": "^1.7.7",

View file

@ -1,6 +1,6 @@
{
"name": "librechat-data-provider",
"version": "0.7.6991",
"version": "0.7.6992",
"description": "data services for librechat apps",
"main": "dist/index.js",
"module": "dist/index.es.js",

View file

@ -457,6 +457,7 @@ export const intefaceSchema = z
prompts: z.boolean().optional(),
agents: z.boolean().optional(),
temporaryChat: z.boolean().optional(),
runCode: z.boolean().optional(),
})
.default({
endpointsMenu: true,
@ -469,6 +470,7 @@ export const intefaceSchema = z
prompts: true,
agents: true,
temporaryChat: true,
runCode: true,
});
export type TInterfaceConfig = z.infer<typeof intefaceSchema>;

View file

@ -34,6 +34,14 @@ export enum PermissionTypes {
* Type for Multi-Conversation Permissions
*/
MULTI_CONVO = 'MULTI_CONVO',
/**
* Type for Temporary Chat
*/
TEMPORARY_CHAT = 'TEMPORARY_CHAT',
/**
* Type for using the "Run Code" LC Code Interpreter API feature
*/
RUN_CODE = 'RUN_CODE',
}
/**
@ -68,7 +76,15 @@ export const agentPermissionsSchema = z.object({
});
export const multiConvoPermissionsSchema = z.object({
[Permissions.USE]: z.boolean().default(false),
[Permissions.USE]: z.boolean().default(true),
});
export const temporaryChatPermissionsSchema = z.object({
[Permissions.USE]: z.boolean().default(true),
});
export const runCodePermissionsSchema = z.object({
[Permissions.USE]: z.boolean().default(true),
});
export const roleSchema = z.object({
@ -77,6 +93,8 @@ export const roleSchema = z.object({
[PermissionTypes.BOOKMARKS]: bookmarkPermissionsSchema,
[PermissionTypes.AGENTS]: agentPermissionsSchema,
[PermissionTypes.MULTI_CONVO]: multiConvoPermissionsSchema,
[PermissionTypes.TEMPORARY_CHAT]: temporaryChatPermissionsSchema,
[PermissionTypes.RUN_CODE]: runCodePermissionsSchema,
});
export type TRole = z.infer<typeof roleSchema>;
@ -84,6 +102,8 @@ export type TAgentPermissions = z.infer<typeof agentPermissionsSchema>;
export type TPromptPermissions = z.infer<typeof promptPermissionsSchema>;
export type TBookmarkPermissions = z.infer<typeof bookmarkPermissionsSchema>;
export type TMultiConvoPermissions = z.infer<typeof multiConvoPermissionsSchema>;
export type TTemporaryChatPermissions = z.infer<typeof temporaryChatPermissionsSchema>;
export type TRunCodePermissions = z.infer<typeof runCodePermissionsSchema>;
const defaultRolesSchema = z.object({
[SystemRoles.ADMIN]: roleSchema.extend({
@ -106,6 +126,12 @@ const defaultRolesSchema = z.object({
[PermissionTypes.MULTI_CONVO]: multiConvoPermissionsSchema.extend({
[Permissions.USE]: z.boolean().default(true),
}),
[PermissionTypes.TEMPORARY_CHAT]: temporaryChatPermissionsSchema.extend({
[Permissions.USE]: z.boolean().default(true),
}),
[PermissionTypes.RUN_CODE]: runCodePermissionsSchema.extend({
[Permissions.USE]: z.boolean().default(true),
}),
}),
[SystemRoles.USER]: roleSchema.extend({
name: z.literal(SystemRoles.USER),
@ -113,6 +139,8 @@ const defaultRolesSchema = z.object({
[PermissionTypes.BOOKMARKS]: bookmarkPermissionsSchema,
[PermissionTypes.AGENTS]: agentPermissionsSchema,
[PermissionTypes.MULTI_CONVO]: multiConvoPermissionsSchema,
[PermissionTypes.TEMPORARY_CHAT]: temporaryChatPermissionsSchema,
[PermissionTypes.RUN_CODE]: runCodePermissionsSchema,
}),
});
@ -123,6 +151,8 @@ export const roleDefaults = defaultRolesSchema.parse({
[PermissionTypes.BOOKMARKS]: {},
[PermissionTypes.AGENTS]: {},
[PermissionTypes.MULTI_CONVO]: {},
[PermissionTypes.TEMPORARY_CHAT]: {},
[PermissionTypes.RUN_CODE]: {},
},
[SystemRoles.USER]: {
name: SystemRoles.USER,
@ -130,5 +160,7 @@ export const roleDefaults = defaultRolesSchema.parse({
[PermissionTypes.BOOKMARKS]: {},
[PermissionTypes.AGENTS]: {},
[PermissionTypes.MULTI_CONVO]: {},
[PermissionTypes.TEMPORARY_CHAT]: {},
[PermissionTypes.RUN_CODE]: {},
},
});