🚦 fix: Restrict Programmatic Tool Execution Maps (#15105)

* fix: restrict programmatic tool execution maps

* chore: bump `@librechat/agents` to v3.6.10

* fix: honor live programmatic caller projections

* style: sort caller capability imports

* test: expect caller projection loader argument

* chore: bump agents sdk to v3.6.11

* refactor: use SDK caller projection type

* style: sort agent handler imports
This commit is contained in:
Danny Avila 2026-08-22 11:02:09 -04:00 committed by GitHub
parent d3e70159ca
commit 89494d45fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 456 additions and 37 deletions

View file

@ -701,7 +701,7 @@ const executeOpenAIChatCompletion = async (envelope, { req, res }) => {
agent never gains sandbox access even if the admin enabled the
capability globally. */
const toolExecuteOptions = {
loadTools: async (toolNames, agentId) => {
loadTools: async (toolNames, agentId, _configurable, callerCapabilityProjection) => {
const ctx = agentToolContexts.get(agentId) ?? agentToolContexts.get(primaryConfig.id) ?? {};
const result = await loadToolsForExecution({
req,
@ -713,6 +713,7 @@ const executeOpenAIChatCompletion = async (envelope, { req, res }) => {
agent: ctx.agent ?? agent,
signal: abortController.signal,
toolRegistry: ctx.toolRegistry,
callerCapabilityProjection,
backgroundToolNames: ctx.backgroundToolNames,
intentToolNames: ctx.intentToolNames,
mcpAvailableTools: ctx.mcpAvailableTools,

View file

@ -1016,7 +1016,7 @@ const executeResponse = async (envelope, { req, res }) => {
// Create tool execute options for event-driven tool execution
const toolExecuteOptions = {
loadTools: async (toolNames, agentId) => {
loadTools: async (toolNames, agentId, _configurable, callerCapabilityProjection) => {
const ctx =
agentToolContexts.get(agentId) ?? agentToolContexts.get(primaryConfig.id) ?? {};
const result = await loadToolsForExecution({
@ -1029,6 +1029,7 @@ const executeResponse = async (envelope, { req, res }) => {
agent: ctx.agent ?? agent,
signal: abortController.signal,
toolRegistry: ctx.toolRegistry,
callerCapabilityProjection,
backgroundToolNames: ctx.backgroundToolNames,
intentToolNames: ctx.intentToolNames,
mcpAvailableTools: ctx.mcpAvailableTools,
@ -1206,7 +1207,7 @@ const executeResponse = async (envelope, { req, res }) => {
const toolEndCallback = createToolEndCallback({ req, res, artifactPromises, streamId: null });
const toolExecuteOptions = {
loadTools: async (toolNames, agentId) => {
loadTools: async (toolNames, agentId, _configurable, callerCapabilityProjection) => {
const ctx =
agentToolContexts.get(agentId) ?? agentToolContexts.get(primaryConfig.id) ?? {};
const result = await loadToolsForExecution({
@ -1219,6 +1220,7 @@ const executeResponse = async (envelope, { req, res }) => {
agent: ctx.agent ?? agent,
signal: abortController.signal,
toolRegistry: ctx.toolRegistry,
callerCapabilityProjection,
backgroundToolNames: ctx.backgroundToolNames,
intentToolNames: ctx.intentToolNames,
mcpAvailableTools: ctx.mcpAvailableTools,

View file

@ -329,7 +329,7 @@ const initializeClient = async ({
const endpointTokenConfigByAgentId = new Map();
const toolExecuteOptions = {
loadTools: async (toolNames, agentId) => {
loadTools: async (toolNames, agentId, _configurable, callerCapabilityProjection) => {
const ctx = agentToolContexts.get(agentId) ?? {};
logger.debug(`[ON_TOOL_EXECUTE] ctx found: ${!!ctx.userMCPAuthMap}, agent: ${ctx.agent?.id}`);
logger.debug(`[ON_TOOL_EXECUTE] toolRegistry size: ${ctx.toolRegistry?.size ?? 'undefined'}`);
@ -344,6 +344,7 @@ const initializeClient = async ({
toolNames,
agent: ctx.agent,
toolRegistry: ctx.toolRegistry,
callerCapabilityProjection,
backgroundToolNames: ctx.backgroundToolNames,
intentToolNames: ctx.intentToolNames,
mcpAvailableTools: ctx.mcpAvailableTools,

View file

@ -43,6 +43,7 @@ const {
AGENT_EXPECTED_MCP_TOOLS_UNAVAILABLE,
isFatalAgentInitializationError,
resolveCodeExecutionContext,
resolveCallerCapabilityProjectionSnapshot,
} = require('@librechat/api');
const {
Time,
@ -1902,6 +1903,7 @@ async function loadAgentTools({
* @param {string} [params.agentResourceType] - Permission resource type for the authorized agent route
* @param {string[]} params.toolNames - Names of tools to load
* @param {Map} [params.toolRegistry] - Tool registry
* @param {unknown} [params.callerCapabilityProjection] - SDK-owned live caller projection
* @param {Record<string, import('@librechat/api').LCAvailableTools>} [params.mcpAvailableTools] - Run-scoped MCP tool definitions
* @param {import('@librechat/api').RequestScopedMCPConnectionStore} [params.requestScopedConnections] - Run-scoped MCP connections
* @param {Record<string, Record<string, string>>} [params.userMCPAuthMap] - User MCP auth map
@ -1922,6 +1924,7 @@ async function loadToolsForExecution({
agentResourceType,
toolNames,
toolRegistry,
callerCapabilityProjection,
backgroundToolNames,
intentToolNames,
mcpAvailableTools,
@ -1943,6 +1946,12 @@ async function loadToolsForExecution({
requestBody: runtimeRequestBody,
requestScopedConnections: mcpRequestScopedConnections,
};
const activeCallerCapabilities = resolveCallerCapabilityProjectionSnapshot(
callerCapabilityProjection,
);
const activeCodeExecutionToolNames = activeCallerCapabilities
? new Set(activeCallerCapabilities.codeExecutionToolNames)
: undefined;
/** Per-agent set of tools that received the injected `run_in_background`
* param; the event-driven executor gates background dispatch and the
* `check_background_task` poll tool on this reliable per-agent channel. */
@ -2104,9 +2113,14 @@ async function loadToolsForExecution({
let ptcOrchestratedToolNames = [];
if (isPTC && toolRegistry) {
ptcOrchestratedToolNames = Array.from(toolRegistry.keys()).filter(
(name) => !specialToolNames.has(name),
);
ptcOrchestratedToolNames = Array.from(toolRegistry.values())
.filter(
(toolDef) =>
!specialToolNames.has(toolDef.name) &&
(toolDef.allowed_callers ?? ['direct']).includes('code_execution') &&
(activeCodeExecutionToolNames == null || activeCodeExecutionToolNames.has(toolDef.name)),
)
.map((toolDef) => toolDef.name);
}
const requestedNonSpecialToolNames = toolNames.filter((name) => !specialToolNames.has(name));
@ -2199,7 +2213,9 @@ async function loadToolsForExecution({
if (
tool.name &&
tool.name !== AgentConstants.PROGRAMMATIC_TOOL_CALLING &&
tool.name !== AgentConstants.BASH_PROGRAMMATIC_TOOL_CALLING
tool.name !== AgentConstants.BASH_PROGRAMMATIC_TOOL_CALLING &&
(toolRegistry.get(tool.name)?.allowed_callers ?? ['direct']).includes('code_execution') &&
(activeCodeExecutionToolNames == null || activeCodeExecutionToolNames.has(tool.name))
) {
ptcToolMap.set(tool.name, tool);
}

View file

@ -2586,7 +2586,9 @@ describe('ToolService - Action Capability Gating', () => {
},
},
};
const toolRegistry = new Map([[mcpTool, { name: mcpTool }]]);
const toolRegistry = new Map([
[mcpTool, { name: mcpTool, allowed_callers: ['code_execution'] }],
]);
mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities));
await loadToolsForExecution({
@ -2609,6 +2611,169 @@ describe('ToolService - Action Capability Gating', () => {
);
});
it('loads only code_execution tools into the PTC execution map', async () => {
const capabilities = [
AgentCapabilities.tools,
AgentCapabilities.programmatic_tools,
AgentCapabilities.execute_code,
];
const req = createMockReq(capabilities);
const programmaticTool = {
name: 'programmatic_tool',
invoke: jest.fn(),
};
const toolRegistry = new Map([
[
programmaticTool.name,
{ name: programmaticTool.name, allowed_callers: ['code_execution'] },
],
['direct_tool', { name: 'direct_tool', allowed_callers: ['direct'] }],
]);
mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities));
mockLoadToolsUtil.mockResolvedValue({
loadedTools: [programmaticTool],
toolContextMap: {},
});
const result = await loadToolsForExecution({
req,
res: {},
agent: { id: 'agent_ptc', tools: [Tools.execute_code] },
toolNames: [Constants.BASH_PROGRAMMATIC_TOOL_CALLING],
toolRegistry,
actionsEnabled: false,
});
expect(mockLoadToolsUtil).toHaveBeenCalledWith(
expect.objectContaining({ tools: ['programmatic_tool'] }),
);
expect([...result.configurable.ptcToolMap.keys()]).toEqual(['programmatic_tool']);
});
it('intersects PTC loading with the SDK live caller projection', async () => {
const capabilities = [
AgentCapabilities.tools,
AgentCapabilities.programmatic_tools,
AgentCapabilities.execute_code,
];
const req = createMockReq(capabilities);
const activeTool = { name: 'active_programmatic_tool', invoke: jest.fn() };
const deferredTool = { name: 'deferred_programmatic_tool', invoke: jest.fn() };
const toolRegistry = new Map([
[activeTool.name, { name: activeTool.name, allowed_callers: ['code_execution'] }],
[
deferredTool.name,
{
name: deferredTool.name,
allowed_callers: ['code_execution'],
defer_loading: true,
},
],
]);
mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities));
mockLoadToolsUtil.mockResolvedValue({
loadedTools: [activeTool],
toolContextMap: {},
});
const result = await loadToolsForExecution({
req,
res: {},
agent: { id: 'agent_ptc', tools: [Tools.execute_code] },
toolNames: [Constants.BASH_PROGRAMMATIC_TOOL_CALLING],
toolRegistry,
callerCapabilityProjection: {
version: 1,
directToolNames: [],
codeExecutionToolNames: [activeTool.name],
directOnlyToolNames: [],
codeExecutionOnlyToolNames: [activeTool.name],
},
actionsEnabled: false,
});
expect(mockLoadToolsUtil).toHaveBeenCalledWith(
expect.objectContaining({ tools: [activeTool.name] }),
);
expect([...result.configurable.ptcToolMap.keys()]).toEqual([activeTool.name]);
});
it('treats an empty versioned caller projection as authoritative', async () => {
const capabilities = [
AgentCapabilities.tools,
AgentCapabilities.programmatic_tools,
AgentCapabilities.execute_code,
];
const req = createMockReq(capabilities);
const toolRegistry = new Map([
[
'deferred_programmatic_tool',
{ name: 'deferred_programmatic_tool', allowed_callers: ['code_execution'] },
],
]);
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,
callerCapabilityProjection: {
version: 1,
directToolNames: [],
codeExecutionToolNames: [],
directOnlyToolNames: [],
codeExecutionOnlyToolNames: [],
},
actionsEnabled: false,
});
expect(mockLoadToolsUtil).not.toHaveBeenCalled();
expect(result.configurable.ptcToolMap).toEqual(new Map());
});
it('falls back to registry projection for unknown snapshot versions', async () => {
const capabilities = [
AgentCapabilities.tools,
AgentCapabilities.programmatic_tools,
AgentCapabilities.execute_code,
];
const req = createMockReq(capabilities);
const programmaticTool = { name: 'programmatic_tool', invoke: jest.fn() };
const toolRegistry = new Map([
[
programmaticTool.name,
{ name: programmaticTool.name, allowed_callers: ['code_execution'] },
],
]);
mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities));
mockLoadToolsUtil.mockResolvedValue({
loadedTools: [programmaticTool],
toolContextMap: {},
});
await loadToolsForExecution({
req,
res: {},
agent: { id: 'agent_ptc', tools: [Tools.execute_code] },
toolNames: [Constants.BASH_PROGRAMMATIC_TOOL_CALLING],
toolRegistry,
callerCapabilityProjection: {
version: 2,
directToolNames: [],
codeExecutionToolNames: [],
directOnlyToolNames: [],
codeExecutionOnlyToolNames: [],
},
actionsEnabled: false,
});
expect(mockLoadToolsUtil).toHaveBeenCalledWith(
expect.objectContaining({ tools: [programmaticTool.name] }),
);
});
it('does not load PTC when programmatic tools capability is disabled', async () => {
const capabilities = [AgentCapabilities.tools, AgentCapabilities.execute_code];
const req = createMockReq(capabilities);