From 7ec1ba246f80940b62a1cae98fff9950c379fcc9 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 18 Apr 2026 11:35:20 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=AF=20fix:=20enforce=20allowedProvider?= =?UTF-8?q?s=20for=20discovered=20sub-agents?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the third P1 codex finding on #12740: `discoverConnectedAgents` forwarded the caller's `endpointOption` verbatim into `initializeAgent`, but on the OpenAI-compat routes that option's `endpoint` is the primary agent's provider (e.g. `openai`), not `agents`. `initializeAgent` only enforces `allowedProviders` when `isAgentsEndpoint(endpointOption.endpoint)` is true, so handoff sub-agents silently bypassed the provider allowlist configured under `endpoints.agents.allowedProviders`. Override `endpointOption.endpoint` to `EModelEndpoint.agents` for every per-sub-agent init call. The primary agent still uses the caller's endpointOption as before — this only affects the BFS-loaded handoff targets. Regression test asserts the override. --- packages/api/src/agents/discovery.spec.ts | 34 +++++++++++++++++++++++ packages/api/src/agents/discovery.ts | 18 ++++++++++-- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/packages/api/src/agents/discovery.spec.ts b/packages/api/src/agents/discovery.spec.ts index f178cc72f7..40cb51d93b 100644 --- a/packages/api/src/agents/discovery.spec.ts +++ b/packages/api/src/agents/discovery.spec.ts @@ -1,3 +1,4 @@ +import { EModelEndpoint } from 'librechat-data-provider'; import type { Agent, GraphEdge } from 'librechat-data-provider'; import type { Response } from 'express'; import type { ServerRequest } from '~/types'; @@ -202,6 +203,39 @@ describe('discoverConnectedAgents', () => { expect(result.edges.map((e) => e.to)).toEqual(['B']); }); + it('forces `endpoint: agents` on sub-agent init so allowedProviders is always enforced', async () => { + const primaryConfig = makeConfig('A', [{ from: 'A', to: 'B', edgeType: 'handoff' }]); + const getAgent = jest.fn(async () => makeAgent('B', [])); + const checkPermission = jest.fn().mockResolvedValue(true); + + await discoverConnectedAgents( + { + req: makeReq(), + res: makeRes(), + primaryConfig, + // Caller passes a non-agents endpoint (e.g. the OpenAI-compat + // controllers do this — endpoint = primary provider) + endpointOption: { endpoint: EModelEndpoint.openAI, model_parameters: {} }, + allowedProviders: new Set(['openai']), + modelsConfig: { openai: ['gpt-4o'] }, + loadTools: jest.fn(), + }, + { + getAgent, + checkPermission, + logViolation: jest.fn(), + db: {} as never, + }, + ); + + // The helper must override `endpoint` to 'agents' so + // `isAgentsEndpoint` is true and the allowedProviders guard in + // `initializeAgent` actually runs for sub-agents. + expect(mockInitializeAgent).toHaveBeenCalled(); + const initArgs = mockInitializeAgent.mock.calls[0][0]; + expect(initArgs.endpointOption.endpoint).toBe(EModelEndpoint.agents); + }); + it('passes the configured resourceType (e.g. REMOTE_AGENT) to checkPermission', async () => { const primaryConfig = makeConfig('A', [{ from: 'A', to: 'B', edgeType: 'handoff' }]); const getAgent = jest.fn(async () => makeAgent('B', [])); diff --git a/packages/api/src/agents/discovery.ts b/packages/api/src/agents/discovery.ts index 9ad9d94161..b1dc9fcd97 100644 --- a/packages/api/src/agents/discovery.ts +++ b/packages/api/src/agents/discovery.ts @@ -1,5 +1,5 @@ import { logger } from '@librechat/data-schemas'; -import { ResourceType, PermissionBits } from 'librechat-data-provider'; +import { ResourceType, PermissionBits, EModelEndpoint } from 'librechat-data-provider'; import type { Agent, GraphEdge, TModelsConfig, TEndpointOption } from 'librechat-data-provider'; import type { Response as ServerResponse } from 'express'; import type { ServerRequest } from '~/types'; @@ -196,6 +196,20 @@ export async function discoverConnectedAgents( throw new Error(validation.error?.message); } + /** + * Force `endpoint: agents` on the per-sub-agent init call so + * `initializeAgent`'s `isAgentsEndpoint`-gated `allowedProviders` + * check always fires for handoff sub-agents, regardless of which + * endpoint the caller entered through. Without this, the OpenAI- + * compat routes (whose `endpointOption.endpoint` is the primary + * provider, not `agents`) would silently bypass the provider + * allowlist configured under `endpoints.agents.allowedProviders`. + */ + const subAgentEndpointOption: Partial = { + ...(endpointOption ?? {}), + endpoint: EModelEndpoint.agents, + }; + const config = await initializeAgent( { req, @@ -205,7 +219,7 @@ export async function discoverConnectedAgents( requestFiles, conversationId, parentMessageId, - endpointOption, + endpointOption: subAgentEndpointOption, allowedProviders, }, db,