🛂 fix: Enforce Actions Capability Gate Across All Event-Driven Tool Loading Paths#12252
Merged
danny-avila merged 6 commits intoMar 16, 2026
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR closes a capability-bypass where event-driven (“definitions-only”) action tools could be exposed and executed even when AgentCapabilities.actions was disabled, by gating both definition exposure and runtime action-tool loading on the actions capability.
Changes:
- Filtered action tool names out of definitions in
loadToolDefinitionsWrapperwhenAgentCapabilities.actionsis disabled. - Gated
loadActionToolsForExecutioninloadToolsForExecutionbehind resolved capabilities, and added a warning when action execution is skipped. - Added unit tests covering action-tool definition filtering and execution gating when actions are disabled.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
api/server/services/ToolService.js |
Adds actions-capability checks for action tool definition exposure and event-driven action tool execution. |
api/server/services/__tests__/ToolService.spec.js |
Adds regression tests to validate action tools are excluded/blocked when actions capability is disabled. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
Comment on lines
+1201
to
+1208
| 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 actionsEnabled = enabledCapabilities.has(AgentCapabilities.actions); |
Comment on lines
+1203
to
+1206
| if (enabledCapabilities.size === 0 && isEphemeralAgentId(agent.id)) { | ||
| enabledCapabilities = new Set( | ||
| appConfig.endpoints?.[EModelEndpoint.agents]?.capabilities ?? defaultAgentCapabilities, | ||
| ); |
Comment on lines
+1252
to
+1254
| logger.warn( | ||
| `[loadToolsForExecution] Capability "${AgentCapabilities.actions}" disabled. ` + | ||
| `Skipping action tool execution. User: ${req.user.id} | Agent: ${agent.id}`, |
Comment on lines
+203
to
+205
| describe('actions capability gating', () => { | ||
| const actionDelimiter = '|'; | ||
|
|
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.
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.
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 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.
461d26a to
a712967
Compare
Replaces a string guess with the canonical constant to avoid fragility if the ephemeral detection heuristic changes.
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.
jcbartle
pushed a commit
to jcbartle/LibreChat
that referenced
this pull request
May 11, 2026
…oading Paths (danny-avila#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 file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Event-driven (definitions-only) tool loading previously returned action tool definitions and allowed action execution without checking
AgentCapabilities.actions, enabling action tools even when actions were administratively disabled. The bypass created a risk of unintended outbound API calls triggered by agents when actions are disabled.Description
Core fix (
ToolService.js)resolveAgentCapabilitieshelper — eliminates the 3x-duplicated capability resolution block (endpoints config lookup + ephemeral fallback). Used byloadToolDefinitionsWrapper,loadAgentTools, andloadToolsForExecution.loadToolDefinitionsWrapper— action tools are now filtered out offilteredToolsbefore reachingloadToolDefinitionswhenAgentCapabilities.actionsis disabled.loadAgentTools(non-definitions path) — replaced the old bypass pattern (!areToolsEnabled && !tool.includes(actionDelimiter)) with explicit action-tool gating:tool.includes(actionDelimiter) → return actionsEnabled. Replaced the latecheckCapability(AgentCapabilities.actions)guard with ahasActionToolscheck to avoid unnecessaryloadActionSetsDB calls and duplicate warnings.loadToolsForExecution— accepts anactionsEnabledparameter (with cache-based fallback viaresolveAgentCapabilities). GatesloadActionToolsForExecutionbehindactionsEnabled. Warning log includes skipped tool names. Removed orphaned@typeJSDoc comment.Type threading (
packages/api)actionsEnabled?: booleantoInitializedAgenttype, theloadToolscallback return type, and theinitializeAgentdestructuring/return so callers can forward the resolved value.Caller wiring
initialize.js— storesactionsEnabledinagentToolContextsfor primary, handoff, and added-convo agents. BackfillsagentToolContextsforprocessAddedConvoparallel agents.openai.jsandresponses.js— passprimaryConfig.actionsEnabledtoloadToolsForExecution.Tests (
ToolService.spec.js)resolveAgentCapabilities,loadAgentTools, andloadToolsForExecutionare called directly with mocked dependencies.actionsEnabledparam forwarding, fallback capability resolution, ephemeral agent fallback (usingConstants.EPHEMERAL_AGENT_ID).Testing
ToolService.spec.jsandinitialize.spec.js.responses.unit.spec.js,openai.spec.js) continue to pass.