mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-17 05:06:32 +01:00
🛂 fix: Enforce Actions Capability Gate Across All Event-Driven Tool Loading Paths (#12252)
* fix: gate action tools by actions capability in all code paths Extract resolveAgentCapabilities helper to eliminate 3x-duplicated capability resolution. Apply early action-tool filtering in both loadToolDefinitionsWrapper and loadAgentTools non-definitions path. Gate loadActionToolsForExecution in loadToolsForExecution behind an actionsEnabled parameter with a cache-based fallback. Replace the late capability guard in loadAgentTools with a hasActionTools check to avoid unnecessary loadActionSets DB calls and duplicate warnings. * fix: thread actionsEnabled through InitializedAgent type Add actionsEnabled to the loadTools callback return type, InitializedAgent, and the initializeAgent destructuring/return so callers can forward the resolved value to loadToolsForExecution without redundant getEndpointsConfig cache lookups. * fix: pass actionsEnabled from callers to loadToolsForExecution Thread actionsEnabled through the agentToolContexts map in initialize.js (primary and handoff agents) and through primaryConfig in the openai.js and responses.js controllers, avoiding per-tool-call capability re-resolution on the hot path. * test: add regression tests for action capability gating Test the real exported functions (resolveAgentCapabilities, loadAgentTools, loadToolsForExecution) with mocked dependencies instead of shadow re-implementations. Covers definition filtering, execution gating, actionsEnabled param forwarding, and fallback capability resolution. * test: use Constants.EPHEMERAL_AGENT_ID in ephemeral fallback test Replaces a string guess with the canonical constant to avoid fragility if the ephemeral detection heuristic changes. * fix: populate agentToolContexts for addedConvo parallel agents After processAddedConvo returns, backfill agentToolContexts for any agents in agentConfigs not already present, so ON_TOOL_EXECUTE for added-convo agents receives actionsEnabled instead of falling back to a per-call cache lookup.
This commit is contained in:
parent
a26eeea592
commit
6f87b49df8
6 changed files with 372 additions and 38 deletions
|
|
@ -64,6 +64,26 @@ const { redactMessage } = require('~/config/parsers');
|
|||
const { findPluginAuthsByKeys } = require('~/models');
|
||||
const { getFlowStateManager } = require('~/config');
|
||||
const { getLogStores } = require('~/cache');
|
||||
|
||||
/**
|
||||
* Resolves the set of enabled agent capabilities from endpoints config,
|
||||
* falling back to app-level or default capabilities for ephemeral agents.
|
||||
* @param {ServerRequest} req
|
||||
* @param {Object} appConfig
|
||||
* @param {string} agentId
|
||||
* @returns {Promise<Set<string>>}
|
||||
*/
|
||||
async function resolveAgentCapabilities(req, appConfig, agentId) {
|
||||
const endpointsConfig = await getEndpointsConfig(req);
|
||||
let capabilities = new Set(endpointsConfig?.[EModelEndpoint.agents]?.capabilities ?? []);
|
||||
if (capabilities.size === 0 && isEphemeralAgentId(agentId)) {
|
||||
capabilities = new Set(
|
||||
appConfig.endpoints?.[EModelEndpoint.agents]?.capabilities ?? defaultAgentCapabilities,
|
||||
);
|
||||
}
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the required actions by calling the appropriate tools and returning the outputs.
|
||||
* @param {OpenAIClient} client - OpenAI or StreamRunManager Client.
|
||||
|
|
@ -445,17 +465,11 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to
|
|||
}
|
||||
|
||||
const appConfig = req.config;
|
||||
const endpointsConfig = await getEndpointsConfig(req);
|
||||
let enabledCapabilities = new Set(endpointsConfig?.[EModelEndpoint.agents]?.capabilities ?? []);
|
||||
|
||||
if (enabledCapabilities.size === 0 && isEphemeralAgentId(agent.id)) {
|
||||
enabledCapabilities = new Set(
|
||||
appConfig.endpoints?.[EModelEndpoint.agents]?.capabilities ?? defaultAgentCapabilities,
|
||||
);
|
||||
}
|
||||
const enabledCapabilities = await resolveAgentCapabilities(req, appConfig, agent.id);
|
||||
|
||||
const checkCapability = (capability) => enabledCapabilities.has(capability);
|
||||
const areToolsEnabled = checkCapability(AgentCapabilities.tools);
|
||||
const actionsEnabled = checkCapability(AgentCapabilities.actions);
|
||||
const deferredToolsEnabled = checkCapability(AgentCapabilities.deferred_tools);
|
||||
|
||||
const filteredTools = agent.tools?.filter((tool) => {
|
||||
|
|
@ -468,7 +482,10 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to
|
|||
if (tool === Tools.web_search) {
|
||||
return checkCapability(AgentCapabilities.web_search);
|
||||
}
|
||||
if (!areToolsEnabled && !tool.includes(actionDelimiter)) {
|
||||
if (tool.includes(actionDelimiter)) {
|
||||
return actionsEnabled;
|
||||
}
|
||||
if (!areToolsEnabled) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
|
@ -765,6 +782,7 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to
|
|||
toolContextMap,
|
||||
toolDefinitions,
|
||||
hasDeferredTools,
|
||||
actionsEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -808,14 +826,7 @@ async function loadAgentTools({
|
|||
}
|
||||
|
||||
const appConfig = req.config;
|
||||
const endpointsConfig = await getEndpointsConfig(req);
|
||||
let enabledCapabilities = new Set(endpointsConfig?.[EModelEndpoint.agents]?.capabilities ?? []);
|
||||
/** Edge case: use defined/fallback capabilities when the "agents" endpoint is not enabled */
|
||||
if (enabledCapabilities.size === 0 && isEphemeralAgentId(agent.id)) {
|
||||
enabledCapabilities = new Set(
|
||||
appConfig.endpoints?.[EModelEndpoint.agents]?.capabilities ?? defaultAgentCapabilities,
|
||||
);
|
||||
}
|
||||
const enabledCapabilities = await resolveAgentCapabilities(req, appConfig, agent.id);
|
||||
const checkCapability = (capability) => {
|
||||
const enabled = enabledCapabilities.has(capability);
|
||||
if (!enabled) {
|
||||
|
|
@ -832,6 +843,7 @@ async function loadAgentTools({
|
|||
return enabled;
|
||||
};
|
||||
const areToolsEnabled = checkCapability(AgentCapabilities.tools);
|
||||
const actionsEnabled = checkCapability(AgentCapabilities.actions);
|
||||
|
||||
let includesWebSearch = false;
|
||||
const _agentTools = agent.tools?.filter((tool) => {
|
||||
|
|
@ -842,7 +854,9 @@ async function loadAgentTools({
|
|||
} else if (tool === Tools.web_search) {
|
||||
includesWebSearch = checkCapability(AgentCapabilities.web_search);
|
||||
return includesWebSearch;
|
||||
} else if (!areToolsEnabled && !tool.includes(actionDelimiter)) {
|
||||
} else if (tool.includes(actionDelimiter)) {
|
||||
return actionsEnabled;
|
||||
} else if (!areToolsEnabled) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
|
@ -947,13 +961,15 @@ async function loadAgentTools({
|
|||
|
||||
agentTools.push(...additionalTools);
|
||||
|
||||
if (!checkCapability(AgentCapabilities.actions)) {
|
||||
const hasActionTools = _agentTools.some((t) => t.includes(actionDelimiter));
|
||||
if (!hasActionTools) {
|
||||
return {
|
||||
toolRegistry,
|
||||
userMCPAuthMap,
|
||||
toolContextMap,
|
||||
toolDefinitions,
|
||||
hasDeferredTools,
|
||||
actionsEnabled,
|
||||
tools: agentTools,
|
||||
};
|
||||
}
|
||||
|
|
@ -969,6 +985,7 @@ async function loadAgentTools({
|
|||
toolContextMap,
|
||||
toolDefinitions,
|
||||
hasDeferredTools,
|
||||
actionsEnabled,
|
||||
tools: agentTools,
|
||||
};
|
||||
}
|
||||
|
|
@ -1101,6 +1118,7 @@ async function loadAgentTools({
|
|||
userMCPAuthMap,
|
||||
toolDefinitions,
|
||||
hasDeferredTools,
|
||||
actionsEnabled,
|
||||
tools: agentTools,
|
||||
};
|
||||
}
|
||||
|
|
@ -1118,9 +1136,11 @@ async function loadAgentTools({
|
|||
* @param {AbortSignal} [params.signal] - Abort signal
|
||||
* @param {Object} params.agent - The agent object
|
||||
* @param {string[]} params.toolNames - Names of tools to load
|
||||
* @param {Map} [params.toolRegistry] - Tool registry
|
||||
* @param {Record<string, Record<string, string>>} [params.userMCPAuthMap] - User MCP auth map
|
||||
* @param {Object} [params.tool_resources] - Tool resources
|
||||
* @param {string|null} [params.streamId] - Stream ID for web search callbacks
|
||||
* @param {boolean} [params.actionsEnabled] - Whether the actions capability is enabled
|
||||
* @returns {Promise<{ loadedTools: Array, configurable: Object }>}
|
||||
*/
|
||||
async function loadToolsForExecution({
|
||||
|
|
@ -1133,11 +1153,17 @@ async function loadToolsForExecution({
|
|||
userMCPAuthMap,
|
||||
tool_resources,
|
||||
streamId = null,
|
||||
actionsEnabled,
|
||||
}) {
|
||||
const appConfig = req.config;
|
||||
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 isPTC = toolNames.includes(AgentConstants.PROGRAMMATIC_TOOL_CALLING);
|
||||
|
||||
|
|
@ -1194,7 +1220,6 @@ async function loadToolsForExecution({
|
|||
const actionToolNames = allToolNamesToLoad.filter((name) => name.includes(actionDelimiter));
|
||||
const regularToolNames = allToolNamesToLoad.filter((name) => !name.includes(actionDelimiter));
|
||||
|
||||
/** @type {Record<string, unknown>} */
|
||||
if (regularToolNames.length > 0) {
|
||||
const includesWebSearch = regularToolNames.includes(Tools.web_search);
|
||||
const webSearchCallbacks = includesWebSearch ? createOnSearchResults(res, streamId) : undefined;
|
||||
|
|
@ -1225,7 +1250,7 @@ async function loadToolsForExecution({
|
|||
}
|
||||
}
|
||||
|
||||
if (actionToolNames.length > 0 && agent) {
|
||||
if (actionToolNames.length > 0 && agent && actionsEnabled) {
|
||||
const actionTools = await loadActionToolsForExecution({
|
||||
req,
|
||||
res,
|
||||
|
|
@ -1235,6 +1260,11 @@ async function loadToolsForExecution({
|
|||
actionToolNames,
|
||||
});
|
||||
allLoadedTools.push(...actionTools);
|
||||
} else if (actionToolNames.length > 0 && agent && !actionsEnabled) {
|
||||
logger.warn(
|
||||
`[loadToolsForExecution] Capability "${AgentCapabilities.actions}" disabled. ` +
|
||||
`Skipping action tool execution. User: ${req.user.id} | Agent: ${agent.id} | Tools: ${actionToolNames.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (isPTC && allLoadedTools.length > 0) {
|
||||
|
|
@ -1395,4 +1425,5 @@ module.exports = {
|
|||
loadAgentTools,
|
||||
loadToolsForExecution,
|
||||
processRequiredActions,
|
||||
resolveAgentCapabilities,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue