diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js index 5624aef65f..b11529205b 100644 --- a/api/server/services/ToolService.js +++ b/api/server/services/ToolService.js @@ -1451,20 +1451,25 @@ async function loadToolsForExecution({ AgentConstants.PROGRAMMATIC_TOOL_CALLING, ].filter((name) => toolNames.includes(name)); const isPTCRequested = ptcToolNames.length > 0; + const isBashToolRequested = toolNames.includes(AgentConstants.BASH_TOOL); + const isLegacyExecuteCodeRequested = toolNames.includes(Tools.execute_code); + const isCodeExecutionToolRequested = isBashToolRequested || isLegacyExecuteCodeRequested; let enabledCapabilities; - if (actionsEnabled === undefined || isPTCRequested) { + if (actionsEnabled === undefined || isPTCRequested || isCodeExecutionToolRequested) { enabledCapabilities = await resolveAgentCapabilities(req, appConfig, agent?.id); } if (actionsEnabled === undefined) { actionsEnabled = enabledCapabilities.has(AgentCapabilities.actions); } + const codeExecutionEnabled = + enabledCapabilities?.has(AgentCapabilities.execute_code) === true && + agent?.tools?.includes(Tools.execute_code) === true; const isPTC = isPTCRequested && enabledCapabilities.has(AgentCapabilities.programmatic_tools) && - enabledCapabilities.has(AgentCapabilities.execute_code) && - agent?.tools?.includes(Tools.execute_code) === true; + codeExecutionEnabled; logger.debug( `[loadToolsForExecution] isToolSearch: ${isToolSearch}, toolRegistry: ${toolRegistry?.size ?? 'undefined'}`, @@ -1498,7 +1503,16 @@ async function loadToolsForExecution({ } } - const isBashTool = toolNames.includes(AgentConstants.BASH_TOOL); + const isBashTool = + isBashToolRequested && + codeExecutionEnabled && + toolRegistry?.has(AgentConstants.BASH_TOOL) === true; + if (isBashToolRequested && !isBashTool) { + logger.warn( + `[loadToolsForExecution] Skipping unregistered or unauthorized ${AgentConstants.BASH_TOOL}. ` + + `User: ${req.user.id} | Agent: ${agent?.id ?? 'unknown'}`, + ); + } if (isBashTool) { try { const bashTool = createBashExecutionTool({ @@ -1535,9 +1549,22 @@ async function loadToolsForExecution({ } const requestedNonSpecialToolNames = toolNames.filter((name) => !specialToolNames.has(name)); + const allowedNonSpecialToolNames = requestedNonSpecialToolNames.filter((name) => { + if (name !== Tools.execute_code) { + return true; + } + const allowed = codeExecutionEnabled && toolRegistry?.has(Tools.execute_code) === true; + if (!allowed) { + logger.warn( + `[loadToolsForExecution] Skipping unregistered or unauthorized ${Tools.execute_code}. ` + + `User: ${req.user.id} | Agent: ${agent?.id ?? 'unknown'}`, + ); + } + return allowed; + }); const allToolNamesToLoad = isPTC - ? [...new Set([...requestedNonSpecialToolNames, ...ptcOrchestratedToolNames])] - : requestedNonSpecialToolNames; + ? [...new Set([...allowedNonSpecialToolNames, ...ptcOrchestratedToolNames])] + : allowedNonSpecialToolNames; const actionToolNames = []; const regularToolNames = []; diff --git a/api/server/services/__tests__/ToolService.spec.js b/api/server/services/__tests__/ToolService.spec.js index 0d69fb92ef..9f496c5f3a 100644 --- a/api/server/services/__tests__/ToolService.spec.js +++ b/api/server/services/__tests__/ToolService.spec.js @@ -1,3 +1,4 @@ +const { Constants: AgentConstants } = require('@librechat/agents'); const { Tools, Constants, @@ -964,6 +965,29 @@ describe('ToolService - Action Capability Gating', () => { const actionToolName = `get_weather${actionDelimiter}api_example_com`; const regularTool = Tools.web_search; + it('does not load code execution tools that were not registered for the agent', async () => { + const capabilities = [ + AgentCapabilities.tools, + AgentCapabilities.web_search, + AgentCapabilities.execute_code, + ]; + const req = createMockReq(capabilities); + const toolRegistry = new Map([[Tools.web_search, { name: Tools.web_search }]]); + mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities)); + + const result = await loadToolsForExecution({ + req, + res: {}, + agent: { id: 'agent_without_code', tools: [Tools.web_search] }, + toolNames: [AgentConstants.BASH_TOOL, Tools.execute_code], + toolRegistry, + actionsEnabled: false, + }); + + expect(result.loadedTools.map((tool) => tool.name)).toEqual([]); + expect(mockLoadToolsUtil).not.toHaveBeenCalled(); + }); + it('loads bash PTC under the legacy programmatic tool name when code capabilities are enabled', async () => { const capabilities = [ AgentCapabilities.tools,