mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-31 17:03:24 +00:00
🛡️ feat: Let Admins Restrict Stateful Workspace Scopes (#14910)
* feat: let admins restrict stateful workspace scopes * fix: enforce stateful scope policy across agent paths * fix: close stateful scope policy activation gaps
This commit is contained in:
parent
6eb2249620
commit
57ea1137f6
30 changed files with 805 additions and 81 deletions
|
|
@ -20,6 +20,7 @@ const {
|
|||
buildAgentContextAttachmentsByAgentId,
|
||||
collectCodeExecutionProfileRoutes,
|
||||
getLazySubagentConfigId,
|
||||
createStatefulCodeEnvironmentPolicyError,
|
||||
} = require('@librechat/api');
|
||||
const {
|
||||
Permissions,
|
||||
|
|
@ -34,6 +35,7 @@ const {
|
|||
MAX_SUBAGENT_GRAPH_NODES,
|
||||
MAX_SUBAGENT_RUN_CONFIGS,
|
||||
isEphemeralAgentId,
|
||||
resolveAllowedStatefulCodeEnvironments,
|
||||
} = require('librechat-data-provider');
|
||||
const {
|
||||
createToolEndCallback,
|
||||
|
|
@ -194,6 +196,9 @@ const initializeClient = async ({
|
|||
const statefulSessionsAvailable = enabledCapabilities.has(
|
||||
AgentCapabilities.stateful_code_sessions,
|
||||
);
|
||||
const allowedStatefulCodeEnvironments = resolveAllowedStatefulCodeEnvironments(
|
||||
appConfig?.endpoints?.[EModelEndpoint.agents]?.statefulCodeSessions?.allowedEnvironments,
|
||||
);
|
||||
const ephemeralSkillsToggle = req.body?.ephemeralAgent?.skills === true;
|
||||
const skillDbMethods = getSkillDbMethods();
|
||||
|
||||
|
|
@ -807,26 +812,36 @@ const initializeClient = async ({
|
|||
),
|
||||
);
|
||||
|
||||
const toLazySubagentMetadata = (agent) => ({
|
||||
id: agent.id,
|
||||
name: agent.name,
|
||||
description: agent.description,
|
||||
provider: agent.provider,
|
||||
model: agent.model,
|
||||
model_parameters: { model: agent.model_parameters?.model },
|
||||
recursion_limit: agent.recursion_limit,
|
||||
subagents: agent.subagents,
|
||||
configId: getLazySubagentConfigId(agent),
|
||||
codeEnvAvailable:
|
||||
codeEnvAvailable === true && agent.tools?.includes(Tools.execute_code) === true,
|
||||
statefulCodeSessions:
|
||||
const toLazySubagentMetadata = (agent) => {
|
||||
const statefulCodeSessions =
|
||||
statefulSessionsAvailable === true &&
|
||||
codeEnvAvailable === true &&
|
||||
agent.stateful_code_sessions === true &&
|
||||
agent.tools?.includes(Tools.execute_code) === true,
|
||||
statefulCodeEnvironment: agent.stateful_code_environment,
|
||||
includeReasoningHistory: getIncludeReasoningHistory(agent),
|
||||
});
|
||||
agent.tools?.includes(Tools.execute_code) === true;
|
||||
const statefulCodeEnvironment = agent.stateful_code_environment ?? 'user';
|
||||
if (
|
||||
statefulCodeSessions &&
|
||||
!allowedStatefulCodeEnvironments.includes(statefulCodeEnvironment)
|
||||
) {
|
||||
throw createStatefulCodeEnvironmentPolicyError(statefulCodeEnvironment);
|
||||
}
|
||||
return {
|
||||
id: agent.id,
|
||||
name: agent.name,
|
||||
description: agent.description,
|
||||
provider: agent.provider,
|
||||
model: agent.model,
|
||||
model_parameters: { model: agent.model_parameters?.model },
|
||||
recursion_limit: agent.recursion_limit,
|
||||
subagents: agent.subagents,
|
||||
configId: getLazySubagentConfigId(agent),
|
||||
codeEnvAvailable:
|
||||
codeEnvAvailable === true && agent.tools?.includes(Tools.execute_code) === true,
|
||||
statefulCodeSessions,
|
||||
statefulCodeEnvironment,
|
||||
includeReasoningHistory: getIncludeReasoningHistory(agent),
|
||||
};
|
||||
};
|
||||
|
||||
const loadSubagentMetadata = async (agentId) => {
|
||||
if (skippedAgentIds.has(agentId)) return null;
|
||||
|
|
|
|||
|
|
@ -62,7 +62,11 @@ jest.mock('~/server/services/ToolService', () => ({
|
|||
loadAgentTools: jest.fn(),
|
||||
loadToolsForExecution: (...args) => mockLoadToolsForExecution(...args),
|
||||
isFatalAgentInitializationError: (error) =>
|
||||
['AGENT_EXPECTED_MCP_TOOLS_UNAVAILABLE', 'resource_recovery_required'].includes(error?.code),
|
||||
[
|
||||
'AGENT_EXPECTED_MCP_TOOLS_UNAVAILABLE',
|
||||
'resource_recovery_required',
|
||||
'stateful_code_environment_not_allowed',
|
||||
].includes(error?.code),
|
||||
}));
|
||||
|
||||
jest.mock('~/server/controllers/ModelController', () => ({
|
||||
|
|
@ -859,6 +863,42 @@ describe('initializeClient — subagent loading', () => {
|
|||
expect(agentClientArgs.agentConfigs.has(SUBAGENT_ID)).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects a disallowed lazy subagent scope before exposing it for prewarm', async () => {
|
||||
const subAgent = await createAgent({
|
||||
id: SUBAGENT_ID,
|
||||
name: 'Disallowed Stateful Subagent',
|
||||
provider: 'openai',
|
||||
model: 'gpt-4',
|
||||
author: new mongoose.Types.ObjectId(),
|
||||
tools: ['execute_code'],
|
||||
stateful_code_sessions: true,
|
||||
stateful_code_environment: 'conversation',
|
||||
});
|
||||
await grantView(subAgent);
|
||||
mockInitializeAgent.mockResolvedValue(
|
||||
makePrimaryConfig({
|
||||
subagents: { enabled: true, allowSelf: false, agent_ids: [SUBAGENT_ID] },
|
||||
}),
|
||||
);
|
||||
const req = makeSubagentReq();
|
||||
req.config.endpoints.agents.capabilities.push('execute_code', 'stateful_code_sessions');
|
||||
req.config.endpoints.agents.statefulCodeSessions = { allowedEnvironments: ['user'] };
|
||||
|
||||
await expect(
|
||||
initializeClient({
|
||||
req,
|
||||
res: {},
|
||||
signal: new AbortController().signal,
|
||||
endpointOption: makeEndpointOption(),
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: ErrorTypes.STATEFUL_CODE_ENVIRONMENT_NOT_ALLOWED,
|
||||
});
|
||||
|
||||
expect(agentClientArgs).toBeUndefined();
|
||||
expect(mockInitializeAgent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('omits a descriptor when its metadata lookup fails without aborting the primary run', async () => {
|
||||
const primaryConfig = makePrimaryConfig({
|
||||
subagents: { enabled: true, allowSelf: false, agent_ids: [SUBAGENT_ID] },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue