mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-03 22:32:42 +00:00
🛡️ fix: Gate Bash PTC Capabilities (#13053)
This commit is contained in:
parent
030dc98a1d
commit
5bab22d236
6 changed files with 213 additions and 15 deletions
|
|
@ -546,6 +546,10 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to
|
|||
const areToolsEnabled = checkCapability(AgentCapabilities.tools);
|
||||
const actionsEnabled = checkCapability(AgentCapabilities.actions);
|
||||
const deferredToolsEnabled = checkCapability(AgentCapabilities.deferred_tools);
|
||||
const programmaticToolsEnabled = enabledCapabilities.has(AgentCapabilities.programmatic_tools);
|
||||
const codeExecutionEnabled =
|
||||
agent.tools?.includes(Tools.execute_code) === true &&
|
||||
enabledCapabilities.has(AgentCapabilities.execute_code);
|
||||
|
||||
const filteredTools = agent.tools?.filter((tool) => {
|
||||
if (tool === Tools.file_search) {
|
||||
|
|
@ -721,6 +725,8 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to
|
|||
tools: filteredTools,
|
||||
toolOptions: agent.tool_options,
|
||||
deferredToolsEnabled,
|
||||
programmaticToolsEnabled,
|
||||
codeExecutionEnabled,
|
||||
},
|
||||
{
|
||||
isBuiltInTool,
|
||||
|
|
@ -775,6 +781,8 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to
|
|||
tools: filteredTools,
|
||||
toolOptions: agent.tool_options,
|
||||
deferredToolsEnabled,
|
||||
programmaticToolsEnabled,
|
||||
codeExecutionEnabled,
|
||||
},
|
||||
{
|
||||
isBuiltInTool,
|
||||
|
|
@ -1003,6 +1011,10 @@ async function loadAgentTools({
|
|||
|
||||
/** Build tool registry from MCP tools and create PTC/tool search tools if configured */
|
||||
const deferredToolsEnabled = checkCapability(AgentCapabilities.deferred_tools);
|
||||
const programmaticToolsEnabled = enabledCapabilities.has(AgentCapabilities.programmatic_tools);
|
||||
const codeExecutionEnabled =
|
||||
agent.tools?.includes(Tools.execute_code) === true &&
|
||||
enabledCapabilities.has(AgentCapabilities.execute_code);
|
||||
const { toolRegistry, toolDefinitions, additionalTools, hasDeferredTools } =
|
||||
await buildToolClassification({
|
||||
loadedTools,
|
||||
|
|
@ -1010,6 +1022,8 @@ async function loadAgentTools({
|
|||
agentId: agent.id,
|
||||
agentToolOptions: agent.tool_options,
|
||||
deferredToolsEnabled,
|
||||
programmaticToolsEnabled,
|
||||
codeExecutionEnabled,
|
||||
authHeaders: () => getCodeApiAuthHeaders(req),
|
||||
});
|
||||
|
||||
|
|
@ -1256,17 +1270,26 @@ async function loadToolsForExecution({
|
|||
const allLoadedTools = [];
|
||||
const configurable = { userMCPAuthMap };
|
||||
|
||||
if (actionsEnabled === undefined) {
|
||||
const enabledCapabilities = await resolveAgentCapabilities(req, appConfig, agent?.id);
|
||||
actionsEnabled = enabledCapabilities.has(AgentCapabilities.actions);
|
||||
}
|
||||
|
||||
const isToolSearch = toolNames.includes(AgentConstants.TOOL_SEARCH);
|
||||
const ptcToolNames = [
|
||||
AgentConstants.BASH_PROGRAMMATIC_TOOL_CALLING,
|
||||
AgentConstants.PROGRAMMATIC_TOOL_CALLING,
|
||||
].filter((name) => toolNames.includes(name));
|
||||
const isPTC = ptcToolNames.length > 0;
|
||||
const isPTCRequested = ptcToolNames.length > 0;
|
||||
|
||||
let enabledCapabilities;
|
||||
if (actionsEnabled === undefined || isPTCRequested) {
|
||||
enabledCapabilities = await resolveAgentCapabilities(req, appConfig, agent?.id);
|
||||
}
|
||||
if (actionsEnabled === undefined) {
|
||||
actionsEnabled = enabledCapabilities.has(AgentCapabilities.actions);
|
||||
}
|
||||
|
||||
const isPTC =
|
||||
isPTCRequested &&
|
||||
enabledCapabilities.has(AgentCapabilities.programmatic_tools) &&
|
||||
enabledCapabilities.has(AgentCapabilities.execute_code) &&
|
||||
agent?.tools?.includes(Tools.execute_code) === true;
|
||||
|
||||
logger.debug(
|
||||
`[loadToolsForExecution] isToolSearch: ${isToolSearch}, toolRegistry: ${toolRegistry?.size ?? 'undefined'}`,
|
||||
|
|
|
|||
|
|
@ -300,14 +300,20 @@ describe('ToolService - Action Capability Gating', () => {
|
|||
const actionToolName = `get_weather${actionDelimiter}api_example_com`;
|
||||
const regularTool = Tools.web_search;
|
||||
|
||||
it('loads bash PTC under the legacy programmatic tool name for event-driven compatibility', async () => {
|
||||
const req = createMockReq([]);
|
||||
it('loads bash PTC under the legacy programmatic tool name when code capabilities are enabled', async () => {
|
||||
const capabilities = [
|
||||
AgentCapabilities.tools,
|
||||
AgentCapabilities.programmatic_tools,
|
||||
AgentCapabilities.execute_code,
|
||||
];
|
||||
const req = createMockReq(capabilities);
|
||||
const toolRegistry = new Map([['custom_tool', { name: 'custom_tool' }]]);
|
||||
mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities));
|
||||
|
||||
const result = await loadToolsForExecution({
|
||||
req,
|
||||
res: {},
|
||||
agent: { id: 'agent_ptc' },
|
||||
agent: { id: 'agent_ptc', tools: [Tools.execute_code] },
|
||||
toolNames: [Constants.PROGRAMMATIC_TOOL_CALLING],
|
||||
toolRegistry,
|
||||
actionsEnabled: false,
|
||||
|
|
@ -320,6 +326,50 @@ describe('ToolService - Action Capability Gating', () => {
|
|||
expect(result.configurable.ptcToolMap.size).toBe(0);
|
||||
});
|
||||
|
||||
it('does not load PTC when programmatic tools capability is disabled', async () => {
|
||||
const capabilities = [AgentCapabilities.tools, AgentCapabilities.execute_code];
|
||||
const req = createMockReq(capabilities);
|
||||
const toolRegistry = new Map([['custom_tool', { name: 'custom_tool' }]]);
|
||||
mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities));
|
||||
|
||||
const result = await loadToolsForExecution({
|
||||
req,
|
||||
res: {},
|
||||
agent: { id: 'agent_ptc', tools: [Tools.execute_code] },
|
||||
toolNames: [Constants.BASH_PROGRAMMATIC_TOOL_CALLING],
|
||||
toolRegistry,
|
||||
actionsEnabled: false,
|
||||
});
|
||||
|
||||
expect(result.loadedTools.map((tool) => tool.name)).toEqual([]);
|
||||
expect(result.configurable.toolRegistry).toBeUndefined();
|
||||
expect(result.configurable.ptcToolMap).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not load PTC when agent did not request execute_code', async () => {
|
||||
const capabilities = [
|
||||
AgentCapabilities.tools,
|
||||
AgentCapabilities.programmatic_tools,
|
||||
AgentCapabilities.execute_code,
|
||||
];
|
||||
const req = createMockReq(capabilities);
|
||||
const toolRegistry = new Map([['custom_tool', { name: 'custom_tool' }]]);
|
||||
mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities));
|
||||
|
||||
const result = await loadToolsForExecution({
|
||||
req,
|
||||
res: {},
|
||||
agent: { id: 'agent_ptc', tools: [] },
|
||||
toolNames: [Constants.BASH_PROGRAMMATIC_TOOL_CALLING],
|
||||
toolRegistry,
|
||||
actionsEnabled: false,
|
||||
});
|
||||
|
||||
expect(result.loadedTools.map((tool) => tool.name)).toEqual([]);
|
||||
expect(result.configurable.toolRegistry).toBeUndefined();
|
||||
expect(result.configurable.ptcToolMap).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should skip action tool loading when actionsEnabled=false', async () => {
|
||||
const req = createMockReq([]);
|
||||
req.config = {};
|
||||
|
|
|
|||
|
|
@ -308,6 +308,8 @@ describe('classification.ts', () => {
|
|||
agentId: 'agent1',
|
||||
agentToolOptions,
|
||||
deferredToolsEnabled: true,
|
||||
programmaticToolsEnabled: true,
|
||||
codeExecutionEnabled: true,
|
||||
definitionsOnly: true,
|
||||
});
|
||||
|
||||
|
|
@ -327,6 +329,8 @@ describe('classification.ts', () => {
|
|||
agentId: 'agent1',
|
||||
agentToolOptions,
|
||||
deferredToolsEnabled: true,
|
||||
programmaticToolsEnabled: true,
|
||||
codeExecutionEnabled: true,
|
||||
definitionsOnly: true,
|
||||
});
|
||||
|
||||
|
|
@ -334,7 +338,7 @@ describe('classification.ts', () => {
|
|||
expect(result.toolRegistry?.has('tool_search')).toBe(true);
|
||||
});
|
||||
|
||||
it('should still add PTC definition when definitionsOnly=true and has programmatic tools', async () => {
|
||||
it('should add PTC definition when definitionsOnly=true and capabilities allow programmatic tools', async () => {
|
||||
const loadedTools: GenericTool[] = [createMCPTool('tool1')];
|
||||
|
||||
const agentToolOptions: AgentToolOptions = {
|
||||
|
|
@ -347,6 +351,8 @@ describe('classification.ts', () => {
|
|||
agentId: 'agent1',
|
||||
agentToolOptions,
|
||||
deferredToolsEnabled: true,
|
||||
programmaticToolsEnabled: true,
|
||||
codeExecutionEnabled: true,
|
||||
definitionsOnly: true,
|
||||
});
|
||||
|
||||
|
|
@ -355,7 +361,7 @@ describe('classification.ts', () => {
|
|||
expect(result.additionalTools.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should create bash PTC tool when agent has programmatic tools', async () => {
|
||||
it('should create bash PTC tool when capabilities allow programmatic tools', async () => {
|
||||
const loadedTools: GenericTool[] = [createMCPTool('tool1')];
|
||||
|
||||
const agentToolOptions: AgentToolOptions = {
|
||||
|
|
@ -367,6 +373,8 @@ describe('classification.ts', () => {
|
|||
userId: 'user1',
|
||||
agentId: 'agent1',
|
||||
agentToolOptions,
|
||||
programmaticToolsEnabled: true,
|
||||
codeExecutionEnabled: true,
|
||||
});
|
||||
|
||||
expect(result.additionalTools.some((t) => t.name === 'run_tools_with_bash')).toBe(true);
|
||||
|
|
@ -374,6 +382,42 @@ describe('classification.ts', () => {
|
|||
expect(result.toolDefinitions.some((d) => d.name === 'run_tools_with_bash')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not add PTC when programmatic tools capability is disabled', async () => {
|
||||
const loadedTools: GenericTool[] = [createMCPTool('tool1')];
|
||||
|
||||
const result = await buildToolClassification({
|
||||
loadedTools,
|
||||
userId: 'user1',
|
||||
agentId: 'agent1',
|
||||
agentToolOptions: {
|
||||
tool1: { allowed_callers: ['code_execution'] },
|
||||
},
|
||||
codeExecutionEnabled: true,
|
||||
});
|
||||
|
||||
expect(result.additionalTools.some((t) => t.name === 'run_tools_with_bash')).toBe(false);
|
||||
expect(result.toolDefinitions.some((d) => d.name === 'run_tools_with_bash')).toBe(false);
|
||||
expect(result.toolRegistry?.has('run_tools_with_bash')).toBe(false);
|
||||
});
|
||||
|
||||
it('should not add PTC when code execution is not enabled for the agent', async () => {
|
||||
const loadedTools: GenericTool[] = [createMCPTool('tool1')];
|
||||
|
||||
const result = await buildToolClassification({
|
||||
loadedTools,
|
||||
userId: 'user1',
|
||||
agentId: 'agent1',
|
||||
agentToolOptions: {
|
||||
tool1: { allowed_callers: ['code_execution'] },
|
||||
},
|
||||
programmaticToolsEnabled: true,
|
||||
});
|
||||
|
||||
expect(result.additionalTools.some((t) => t.name === 'run_tools_with_bash')).toBe(false);
|
||||
expect(result.toolDefinitions.some((d) => d.name === 'run_tools_with_bash')).toBe(false);
|
||||
expect(result.toolRegistry?.has('run_tools_with_bash')).toBe(false);
|
||||
});
|
||||
|
||||
it('should create tool instances when definitionsOnly=false (default)', async () => {
|
||||
const loadedTools: GenericTool[] = [createMCPTool('tool1')];
|
||||
|
||||
|
|
|
|||
|
|
@ -185,6 +185,10 @@ export interface BuildToolClassificationParams {
|
|||
agentToolOptions?: AgentToolOptions;
|
||||
/** Whether the deferred_tools capability is enabled (from agent config) */
|
||||
deferredToolsEnabled?: boolean;
|
||||
/** Whether the programmatic_tools capability is enabled (from agent config) */
|
||||
programmaticToolsEnabled?: boolean;
|
||||
/** Whether code execution is enabled and requested by this agent */
|
||||
codeExecutionEnabled?: boolean;
|
||||
/** When true, skip creating tool instances (for event-driven mode) */
|
||||
definitionsOnly?: boolean;
|
||||
/** Optional host-supplied Code API auth headers for remote programmatic execution. */
|
||||
|
|
@ -238,7 +242,7 @@ export function agentHasDeferredTools(toolRegistry: LCToolRegistry): boolean {
|
|||
* 1. Filters loaded tools for MCP tools
|
||||
* 2. Extracts tool definitions and builds the registry from agent's tool_options
|
||||
* 3. Cleans up temporary mcpJsonSchema properties
|
||||
* 4. Creates PTC tool only if agent has tools configured for programmatic calling
|
||||
* 4. Creates PTC tool only if capabilities allow the agent's programmatic tools
|
||||
* 5. Creates tool search tool only if agent has deferred tools
|
||||
*
|
||||
* @param params - Parameters including loaded tools, userId, agentId, agentToolOptions, and dependencies
|
||||
|
|
@ -253,6 +257,8 @@ export async function buildToolClassification(
|
|||
agentToolOptions,
|
||||
definitionsOnly = false,
|
||||
deferredToolsEnabled = true,
|
||||
programmaticToolsEnabled = false,
|
||||
codeExecutionEnabled = false,
|
||||
authHeaders,
|
||||
} = params;
|
||||
const additionalTools: GenericTool[] = [];
|
||||
|
|
@ -275,10 +281,11 @@ export async function buildToolClassification(
|
|||
|
||||
/**
|
||||
* Check if this agent actually has tools configured for these features.
|
||||
* Only enable PTC if the agent has programmatic tools.
|
||||
* Only enable PTC if code/programmatic capabilities allow the agent's programmatic tools.
|
||||
* Only enable tool search if the agent has deferred tools AND the capability is enabled.
|
||||
*/
|
||||
const hasProgrammaticTools = agentHasProgrammaticTools(toolRegistry);
|
||||
const hasProgrammaticTools =
|
||||
programmaticToolsEnabled && codeExecutionEnabled && agentHasProgrammaticTools(toolRegistry);
|
||||
const hasDeferredTools = deferredToolsEnabled && agentHasDeferredTools(toolRegistry);
|
||||
|
||||
/** Clear defer_loading if capability disabled */
|
||||
|
|
|
|||
|
|
@ -309,6 +309,66 @@ describe('definitions.ts', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('programmatic tool calling capability gate', () => {
|
||||
const mcpToolName = 'run_report_mcp_server_one';
|
||||
const serverTools = {
|
||||
[mcpToolName]: {
|
||||
function: {
|
||||
name: mcpToolName,
|
||||
description: 'Run report',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('does not add Bash PTC definitions unless both code and programmatic capabilities are enabled', async () => {
|
||||
mockGetOrFetchMCPServerTools.mockResolvedValueOnce(serverTools);
|
||||
|
||||
const result = await loadToolDefinitions(
|
||||
{
|
||||
userId: 'user-123',
|
||||
agentId: 'agent-123',
|
||||
tools: [mcpToolName],
|
||||
toolOptions: {
|
||||
[mcpToolName]: { allowed_callers: ['code_execution'] },
|
||||
},
|
||||
codeExecutionEnabled: true,
|
||||
},
|
||||
{
|
||||
getOrFetchMCPServerTools: mockGetOrFetchMCPServerTools,
|
||||
isBuiltInTool: mockIsBuiltInTool,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.toolDefinitions.some((d) => d.name === 'run_tools_with_bash')).toBe(false);
|
||||
expect(result.toolRegistry.has('run_tools_with_bash')).toBe(false);
|
||||
});
|
||||
|
||||
it('adds Bash PTC definitions when code and programmatic capabilities are enabled', async () => {
|
||||
mockGetOrFetchMCPServerTools.mockResolvedValueOnce(serverTools);
|
||||
|
||||
const result = await loadToolDefinitions(
|
||||
{
|
||||
userId: 'user-123',
|
||||
agentId: 'agent-123',
|
||||
tools: [mcpToolName],
|
||||
toolOptions: {
|
||||
[mcpToolName]: { allowed_callers: ['code_execution'] },
|
||||
},
|
||||
programmaticToolsEnabled: true,
|
||||
codeExecutionEnabled: true,
|
||||
},
|
||||
{
|
||||
getOrFetchMCPServerTools: mockGetOrFetchMCPServerTools,
|
||||
isBuiltInTool: mockIsBuiltInTool,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.toolDefinitions.some((d) => d.name === 'run_tools_with_bash')).toBe(true);
|
||||
expect(result.toolRegistry.has('run_tools_with_bash')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MCP tool definitions with server name variants', () => {
|
||||
it('should load MCP tools with underscored server names (server_one)', async () => {
|
||||
const mockServerTools = {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@ export interface LoadToolDefinitionsParams {
|
|||
toolOptions?: AgentToolOptions;
|
||||
/** Whether deferred tools feature is enabled */
|
||||
deferredToolsEnabled?: boolean;
|
||||
/** Whether programmatic tool calling is enabled */
|
||||
programmaticToolsEnabled?: boolean;
|
||||
/** Whether code execution is enabled and requested by this agent */
|
||||
codeExecutionEnabled?: boolean;
|
||||
}
|
||||
|
||||
export interface ActionToolDefinition {
|
||||
|
|
@ -71,7 +75,15 @@ export async function loadToolDefinitions(
|
|||
params: LoadToolDefinitionsParams,
|
||||
deps: LoadToolDefinitionsDeps,
|
||||
): Promise<LoadToolDefinitionsResult> {
|
||||
const { userId, agentId, tools, toolOptions = {}, deferredToolsEnabled = false } = params;
|
||||
const {
|
||||
userId,
|
||||
agentId,
|
||||
tools,
|
||||
toolOptions = {},
|
||||
deferredToolsEnabled = false,
|
||||
programmaticToolsEnabled = false,
|
||||
codeExecutionEnabled = false,
|
||||
} = params;
|
||||
const { getOrFetchMCPServerTools, isBuiltInTool, getActionToolDefinitions } = deps;
|
||||
|
||||
const emptyResult: LoadToolDefinitionsResult = {
|
||||
|
|
@ -191,6 +203,8 @@ export async function loadToolDefinitions(
|
|||
agentId,
|
||||
loadedTools,
|
||||
deferredToolsEnabled,
|
||||
programmaticToolsEnabled,
|
||||
codeExecutionEnabled,
|
||||
definitionsOnly: true,
|
||||
agentToolOptions: toolOptions,
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue