🧯 fix: enforce allowedProviders for discovered sub-agents

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.
This commit is contained in:
Danny Avila 2026-04-18 11:35:20 -04:00
parent e491a23a77
commit 7ec1ba246f
2 changed files with 50 additions and 2 deletions

View file

@ -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', []));

View file

@ -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<TEndpointOption> = {
...(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,