🚷 fix: Reject Client-Supplied Subagent Configuration (#13660)

This commit is contained in:
Danny Avila
2026-06-10 16:10:59 -04:00
committed by GitHub
parent eebbde777a
commit a52c82489e
6 changed files with 23 additions and 72 deletions

View File

@@ -126,7 +126,7 @@ describe('applyModelSpecEphemeralAgent', () => {
applyModelSpecEphemeralAgent({ convoId: null, modelSpec, updateEphemeralAgent });
const agent = updateEphemeralAgent.mock.calls[0][1] as TEphemeralAgent;
expect(agent.subagents).toBeUndefined();
expect('subagents' in agent).toBe(false);
});
});

View File

@@ -1,11 +1,12 @@
import mongoose from 'mongoose';
import { v4 as uuidv4 } from 'uuid';
import { MongoMemoryServer } from 'mongodb-memory-server';
import { Constants, FileSources } from 'librechat-data-provider';
import { agentSchema, createMethods } from '@librechat/data-schemas';
import { Constants, FileSources, MAX_SUBAGENTS } from 'librechat-data-provider';
import type {
Agent as LibreChatAgent,
AgentModelParameters,
TEphemeralAgent,
TConversation,
} from 'librechat-data-provider';
import type { LoadAgentParams, LoadAgentDeps } from '../load';
@@ -266,7 +267,9 @@ describe('loadAgent', () => {
req: {
user: { id: 'user123' },
body: {
ephemeralAgent: { subagents: { enabled: false, agent_ids: ['agent_tampered'] } },
ephemeralAgent: {
subagents: { enabled: false, agent_ids: ['agent_tampered'] },
} as unknown as TEphemeralAgent,
},
config: {
config: {},
@@ -294,6 +297,7 @@ describe('loadAgent', () => {
expect(result?.skills_enabled).toBe(true);
expect(result?.skills).toBeUndefined();
expect(result?.subagents).toBeUndefined();
});
test('should initialize an empty allowlist for ephemeral model spec skill names', async () => {
@@ -368,9 +372,8 @@ describe('loadAgent', () => {
expect(result?.subagents).toEqual(subagents);
});
test('should discard oversized request subagent ids for ephemeral agents', async () => {
test('should ignore request subagents for ephemeral agents', async () => {
const { EPHEMERAL_AGENT_ID } = Constants;
const oversized = Array.from({ length: MAX_SUBAGENTS + 1 }, (_, index) => `agent_${index}`);
const result = await loadAgent(
{
@@ -378,8 +381,8 @@ describe('loadAgent', () => {
user: { id: 'user123' },
body: {
ephemeralAgent: {
subagents: { enabled: true, allowSelf: false, agent_ids: oversized },
},
subagents: { enabled: true, allowSelf: true, agent_ids: ['agent_other'] },
} as unknown as TEphemeralAgent,
},
},
agent_id: EPHEMERAL_AGENT_ID as string,
@@ -389,12 +392,11 @@ describe('loadAgent', () => {
deps,
);
expect(result?.subagents).toEqual({ enabled: true, allowSelf: false });
expect(result?.subagents).toBeUndefined();
});
test('should preserve request subagents when added agent mirrors ephemeral primary tools', async () => {
test('should ignore request subagents when added agent mirrors ephemeral primary tools', async () => {
const { EPHEMERAL_AGENT_ID } = Constants;
const subagents = { enabled: true, allowSelf: true, agent_ids: [] };
const result = await loadAddedAgent(
{
@@ -409,7 +411,7 @@ describe('loadAgent', () => {
conversation: {
endpoint: 'openai',
model: 'gpt-4',
ephemeralAgent: { subagents },
ephemeralAgent: { subagents: { enabled: true, allowSelf: true, agent_ids: [] } },
} as unknown as TConversation,
primaryAgent: { id: EPHEMERAL_AGENT_ID as string, tools: ['web_search'] } as LibreChatAgent,
},
@@ -417,13 +419,10 @@ describe('loadAgent', () => {
);
expect(result?.tools).toEqual(['web_search']);
expect(result?.subagents).toEqual(subagents);
expect(result?.subagents).toBeUndefined();
});
test('should discard oversized request subagent ids for mirrored added agents', async () => {
const { EPHEMERAL_AGENT_ID } = Constants;
const oversized = Array.from({ length: MAX_SUBAGENTS + 1 }, (_, index) => `agent_${index}`);
test('should ignore request subagents for added ephemeral agents', async () => {
const result = await loadAddedAgent(
{
req: {
@@ -437,17 +436,13 @@ describe('loadAgent', () => {
conversation: {
endpoint: 'openai',
model: 'gpt-4',
ephemeralAgent: {
subagents: { enabled: true, allowSelf: false, agent_ids: oversized },
},
ephemeralAgent: { subagents: { enabled: true, allowSelf: true, agent_ids: [] } },
} as unknown as TConversation,
primaryAgent: { id: EPHEMERAL_AGENT_ID as string, tools: ['web_search'] } as LibreChatAgent,
},
deps,
);
expect(result?.tools).toEqual(['web_search']);
expect(result?.subagents).toEqual({ enabled: true, allowSelf: false });
expect(result?.subagents).toBeUndefined();
});
test('should enable full skill scope for added ephemeral model spec with skills true', async () => {

View File

@@ -7,15 +7,9 @@ import {
appendAgentIdSuffix,
encodeEphemeralAgentId,
} from 'librechat-data-provider';
import type {
Agent,
TConversation,
TModelSpec,
AgentSubagentsConfig,
} from 'librechat-data-provider';
import type { Agent, TConversation, TModelSpec } from 'librechat-data-provider';
import type { AppConfig } from '@librechat/data-schemas';
import { getCustomEndpointConfig } from '~/app/config';
import { sanitizeRequestSubagents } from './subagents';
const { mcp_all, mcp_delimiter } = Constants;
@@ -43,11 +37,9 @@ function applyModelSpecSkills(
function applyModelSpecSubagents(
result: Record<string, unknown>,
modelSpec: Pick<TModelSpec, 'subagents'> | null | undefined,
ephemeralAgent?: { subagents?: AgentSubagentsConfig },
): void {
const subagents = modelSpec?.subagents ?? sanitizeRequestSubagents(ephemeralAgent?.subagents);
if (subagents) {
result.subagents = subagents;
if (modelSpec?.subagents) {
result.subagents = modelSpec.subagents;
}
}
@@ -105,7 +97,6 @@ export async function loadAddedAgent(
file_search?: boolean;
web_search?: boolean;
artifacts?: unknown;
subagents?: AgentSubagentsConfig;
};
[key: string]: unknown;
};
@@ -123,7 +114,6 @@ export async function loadAddedAgent(
file_search?: boolean;
web_search?: boolean;
artifacts?: unknown;
subagents?: AgentSubagentsConfig;
}
| undefined;
@@ -160,7 +150,7 @@ export async function loadAddedAgent(
tools: [...primaryAgent.tools],
};
applyModelSpecSkills(result, modelSpec);
applyModelSpecSubagents(result, modelSpec, ephemeralAgent);
applyModelSpecSubagents(result, modelSpec);
return result as unknown as Agent;
}
@@ -254,7 +244,7 @@ export async function loadAddedAgent(
if (ephemeralAgent?.artifacts != null && ephemeralAgent.artifacts) {
result.artifacts = ephemeralAgent.artifacts;
}
applyModelSpecSubagents(result, modelSpec, ephemeralAgent);
applyModelSpecSubagents(result, modelSpec);
applyModelSpecSkills(result, modelSpec);
return result as unknown as Agent;

View File

@@ -14,7 +14,6 @@ import type {
} from 'librechat-data-provider';
import type { AppConfig } from '@librechat/data-schemas';
import { getCustomEndpointConfig } from '~/app/config';
import { sanitizeRequestSubagents } from './subagents';
const { mcp_all, mcp_delimiter } = Constants;
type ModelParametersWithPromptPrefix = AgentModelParameters & { promptPrefix?: string | null };
@@ -138,11 +137,6 @@ export async function loadEphemeralAgent(
}
if (modelSpec?.subagents) {
result.subagents = modelSpec.subagents;
} else {
const requestSubagents = sanitizeRequestSubagents(ephemeralAgent?.subagents);
if (requestSubagents) {
result.subagents = requestSubagents;
}
}
if (modelSpec && Object.prototype.hasOwnProperty.call(modelSpec, 'skills')) {
if (modelSpec.skills === true) {

View File

@@ -1,27 +0,0 @@
import { MAX_SUBAGENTS } from 'librechat-data-provider';
import type { AgentSubagentsConfig } from 'librechat-data-provider';
export function sanitizeRequestSubagents(
subagents?: AgentSubagentsConfig | null,
): AgentSubagentsConfig | undefined {
if (!subagents || typeof subagents !== 'object') {
return undefined;
}
const sanitized: AgentSubagentsConfig = {};
if (typeof subagents.enabled === 'boolean') {
sanitized.enabled = subagents.enabled;
}
if (typeof subagents.allowSelf === 'boolean') {
sanitized.allowSelf = subagents.allowSelf;
}
if (
Array.isArray(subagents.agent_ids) &&
subagents.agent_ids.length <= MAX_SUBAGENTS &&
subagents.agent_ids.every((agentId) => typeof agentId === 'string')
) {
sanitized.agent_ids = subagents.agent_ids;
}
return Object.keys(sanitized).length > 0 ? sanitized : undefined;
}

View File

@@ -10,11 +10,11 @@ import type {
ReasoningResponseKey,
ReasoningParameterFormat,
} from './schemas';
import type { Agent, AgentSubagentsConfig } from './types/assistants';
import type { RefillIntervalUnit } from './balance';
import type { SettingDefinition } from './generate';
import type { TMinimalFeedback } from './feedback';
import type { ContentTypes } from './types/runs';
import type { Agent } from './types/assistants';
export * from './schemas';
@@ -107,7 +107,6 @@ export type TEphemeralAgent = {
execute_code?: boolean;
artifacts?: string;
skills?: boolean;
subagents?: AgentSubagentsConfig;
};
export type TPayload = Partial<TMessage> &