diff --git a/api/app/clients/tools/util/handleTools.js b/api/app/clients/tools/util/handleTools.js
index ec75adea76..a982d698e1 100644
--- a/api/app/clients/tools/util/handleTools.js
+++ b/api/app/clients/tools/util/handleTools.js
@@ -356,6 +356,9 @@ const loadTools = async ({
resolveCodeExecutionContext({
statefulSessions,
environment: agent?.stateful_code_environment,
+ environmentId: agent?.code_environment_id,
+ environments:
+ options.req?.config?.endpoints?.agents?.statefulCodeSessions?.environments,
userId: user,
agentId: agent?.id,
conversationId: options.req?.body?.conversationId,
@@ -365,6 +368,7 @@ const loadTools = async ({
agentId: agent?.id,
codeApiBaseUrl: codeExecutionContext.baseUrl,
executionProfile: codeExecutionContext.executionProfile,
+ executionRouteKey: codeExecutionContext.executionRouteKey,
});
if (toolContext) {
dynamicToolContextMap[tool] = toolContext;
diff --git a/api/server/controllers/agents/__tests__/callbacks.spec.js b/api/server/controllers/agents/__tests__/callbacks.spec.js
index a4195dccd4..aec8c48a9a 100644
--- a/api/server/controllers/agents/__tests__/callbacks.spec.js
+++ b/api/server/controllers/agents/__tests__/callbacks.spec.js
@@ -855,6 +855,7 @@ describe('createToolEndCallback', () => {
codeExecutionContext: {
baseUrl: 'https://code-stateful.example.com',
executionProfile: 'stateful',
+ executionRouteKey: `stateful:${'a'.repeat(32)}`,
},
});
await toolEndCallback({ output: event.output }, event.metadata);
@@ -870,6 +871,7 @@ describe('createToolEndCallback', () => {
conversationId: 'thread789',
codeApiBaseUrl: 'https://code-stateful.example.com',
executionProfile: 'stateful',
+ executionRouteKey: `stateful:${'a'.repeat(32)}`,
}),
);
expect(res.write).toHaveBeenCalledTimes(2);
diff --git a/api/server/controllers/agents/callbacks.js b/api/server/controllers/agents/callbacks.js
index 864273c0a8..5f3579fd34 100644
--- a/api/server/controllers/agents/callbacks.js
+++ b/api/server/controllers/agents/callbacks.js
@@ -1047,6 +1047,7 @@ function createToolEndCallback({ req, res, artifactPromises, streamId = null, jo
session_id: sessionId,
codeApiBaseUrl: metadata.codeExecutionContext?.baseUrl,
executionProfile: metadata.codeExecutionContext?.executionProfile,
+ executionRouteKey: metadata.codeExecutionContext?.executionRouteKey,
preparedBuffer,
downloadFallback,
});
@@ -1391,6 +1392,7 @@ function createResponsesToolEndCallback({ req, res, tracker, artifactPromises })
session_id: sessionId,
codeApiBaseUrl: metadata.codeExecutionContext?.baseUrl,
executionProfile: metadata.codeExecutionContext?.executionProfile,
+ executionRouteKey: metadata.codeExecutionContext?.executionRouteKey,
preparedBuffer,
downloadFallback,
});
diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js
index 7ce1bde2e1..042fbcd3b9 100644
--- a/api/server/controllers/agents/client.js
+++ b/api/server/controllers/agents/client.js
@@ -404,6 +404,9 @@ class AgentClient extends BaseClient {
hide_sequential_outputs: agent.hide_sequential_outputs,
stateful_code_sessions: agent.stateful_code_sessions,
stateful_code_environment: agent.stateful_code_environment,
+ execution_route_key:
+ agent.codeExecutionContext?.executionRouteKey ??
+ agent.codeExecutionContext?.executionProfile,
artifacts: agent.artifacts,
recursion_limit: agent.recursion_limit,
subagents: agent.subagents,
diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js
index a9a1158149..f412b152ff 100644
--- a/api/server/controllers/agents/v1.js
+++ b/api/server/controllers/agents/v1.js
@@ -443,7 +443,27 @@ const isCodeInterpreterCapabilityEnabled = (req) => {
/** Reject a newly selected stateful workspace scope that the deployment owner
* has excluded. Disabled sessions and unrelated edits remain saveable so an
* allowlist tightening never silently rewrites or strands an existing agent. */
-const validateStatefulCodeEnvironment = (req, res, enabled, environment) => {
+const validateStatefulCodeEnvironment = (
+ req,
+ res,
+ enabled,
+ environment,
+ environmentId,
+ environmentIdSelected = false,
+) => {
+ if (enabled !== true && !environmentIdSelected) {
+ return true;
+ }
+ if (environmentId != null) {
+ const configuredEnvironments =
+ req.config?.endpoints?.[EModelEndpoint.agents]?.statefulCodeSessions?.environments ?? [];
+ if (!configuredEnvironments.some((configured) => configured.id === environmentId)) {
+ res.status(400).json({
+ error: `Stateful code environment is not configured: ${environmentId}`,
+ });
+ return false;
+ }
+ }
if (enabled !== true) {
return true;
}
@@ -721,6 +741,8 @@ const createAgentHandler = async (req, res) => {
res,
agentData.stateful_code_sessions,
agentData.stateful_code_environment,
+ agentData.code_environment_id,
+ agentData.code_environment_id != null,
)
) {
return;
@@ -997,13 +1019,22 @@ const updateAgentHandler = async (req, res) => {
normalizeToolResourceFiles(req.body?.tool_resources);
const validatedData = agentUpdateSchema.parse(req.body);
// Preserve explicit null for avatar to allow resetting the avatar
- const { avatar: avatarField, _id, ...rest } = validatedData;
+ const {
+ avatar: avatarField,
+ code_environment_id: codeEnvironmentIdField,
+ _id,
+ ...rest
+ } = validatedData;
const updateData = removeNullishValues(rest);
+ if (codeEnvironmentIdField !== undefined) {
+ updateData.code_environment_id = codeEnvironmentIdField;
+ }
let existingAgent;
const includesStatefulConfiguration =
updateData.stateful_code_sessions !== undefined ||
- updateData.stateful_code_environment !== undefined;
+ updateData.stateful_code_environment !== undefined ||
+ updateData.code_environment_id !== undefined;
const includesToolsConfiguration = Array.isArray(updateData.tools);
const includesToolOptionsConfiguration = updateData.tool_options !== undefined;
if (
@@ -1016,13 +1047,17 @@ const updateAgentHandler = async (req, res) => {
return res.status(404).json({ error: 'Agent not found' });
}
+ const codeEnvironmentSelectionChanged =
+ updateData.code_environment_id !== undefined &&
+ updateData.code_environment_id !== existingAgent.code_environment_id;
const statefulConfigurationChanged =
(updateData.stateful_code_sessions !== undefined &&
(updateData.stateful_code_sessions === true) !==
(existingAgent.stateful_code_sessions === true)) ||
(updateData.stateful_code_environment !== undefined &&
(updateData.stateful_code_environment ?? 'user') !==
- (existingAgent.stateful_code_environment ?? 'user'));
+ (existingAgent.stateful_code_environment ?? 'user')) ||
+ codeEnvironmentSelectionChanged;
const activatesCodeExecution =
includesToolsConfiguration &&
updateData.tools.includes(Tools.execute_code) &&
@@ -1032,12 +1067,18 @@ const updateAgentHandler = async (req, res) => {
updateData.stateful_code_sessions ?? existingAgent.stateful_code_sessions;
const effectiveStatefulEnvironment =
updateData.stateful_code_environment ?? existingAgent.stateful_code_environment;
+ const effectiveCodeEnvironmentId =
+ updateData.code_environment_id === null
+ ? undefined
+ : (updateData.code_environment_id ?? existingAgent.code_environment_id);
if (
!validateStatefulCodeEnvironment(
req,
res,
effectiveStatefulSessions,
effectiveStatefulEnvironment,
+ effectiveCodeEnvironmentId,
+ codeEnvironmentSelectionChanged,
)
) {
return;
@@ -1227,6 +1268,11 @@ const updateAgentHandler = async (req, res) => {
}
}
+ if (updateData.code_environment_id === null) {
+ delete updateData.code_environment_id;
+ updateData.$unset = { code_environment_id: 1 };
+ }
+
let updatedAgent =
Object.keys(updateData).length > 0
? await db.updateAgent({ id }, updateData, {
@@ -1333,6 +1379,7 @@ const duplicateAgentHandler = async (req, res) => {
res,
newAgentData.stateful_code_sessions,
newAgentData.stateful_code_environment,
+ newAgentData.code_environment_id,
)
) {
return;
@@ -1925,6 +1972,7 @@ const revertAgentVersionHandler = async (req, res) => {
res,
revertVersion.stateful_code_sessions,
revertVersion.stateful_code_environment,
+ revertVersion.code_environment_id,
)
) {
return;
diff --git a/api/server/controllers/agents/v1.spec.js b/api/server/controllers/agents/v1.spec.js
index 02b1f3e834..ef1345586f 100644
--- a/api/server/controllers/agents/v1.spec.js
+++ b/api/server/controllers/agents/v1.spec.js
@@ -237,6 +237,39 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
expect(await Agent.countDocuments()).toBe(0);
});
+ test('rejects an unconfigured code environment id', async () => {
+ mockReq.config = {
+ endpoints: {
+ agents: {
+ statefulCodeSessions: {
+ allowedEnvironments: ['user'],
+ environments: [
+ {
+ id: 'configured-vm',
+ name: 'Configured VM',
+ type: 'attached',
+ baseURL: 'https://code.example.com/v1',
+ default: true,
+ },
+ ],
+ },
+ },
+ },
+ };
+ mockReq.body = {
+ name: 'Invalid Environment Agent',
+ provider: 'openai',
+ model: 'gpt-4',
+ stateful_code_sessions: true,
+ code_environment_id: 'missing-vm',
+ };
+
+ await createAgentHandler(mockReq, mockRes);
+
+ expect(mockRes.status).toHaveBeenCalledWith(400);
+ expect(await Agent.countDocuments()).toBe(0);
+ });
+
test('should block configured agent instruction content before persistence', async () => {
mockReq.config = {
filters: {
@@ -1182,6 +1215,98 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
expect(agentInDb.stateful_code_sessions).not.toBe(true);
});
+ test('rejects updating an agent to an unconfigured code environment id', async () => {
+ mockReq.user.id = existingAgentAuthorId.toString();
+ mockReq.params.id = existingAgentId;
+ mockReq.config = {
+ endpoints: {
+ agents: {
+ statefulCodeSessions: {
+ allowedEnvironments: ['user'],
+ environments: [],
+ },
+ },
+ },
+ };
+ mockReq.body = { code_environment_id: 'missing-vm' };
+
+ await updateAgentHandler(mockReq, mockRes);
+
+ expect(mockRes.status).toHaveBeenCalledWith(400);
+ const agentInDb = await Agent.findOne({ id: existingAgentId });
+ expect(agentInDb.code_environment_id).toBeUndefined();
+ });
+
+ test('allows disabling stateful sessions after the configured environment is removed', async () => {
+ await Agent.updateOne(
+ { id: existingAgentId },
+ {
+ stateful_code_sessions: true,
+ code_environment_id: 'removed-vm',
+ },
+ );
+ mockReq.user.id = existingAgentAuthorId.toString();
+ mockReq.params.id = existingAgentId;
+ mockReq.config = {
+ endpoints: {
+ agents: {
+ statefulCodeSessions: {
+ allowedEnvironments: ['user'],
+ environments: [],
+ },
+ },
+ },
+ };
+ mockReq.body = {
+ stateful_code_sessions: false,
+ code_environment_id: 'removed-vm',
+ };
+
+ await updateAgentHandler(mockReq, mockRes);
+
+ expect(mockRes.status).not.toHaveBeenCalledWith(400);
+ const agentInDb = await Agent.findOne({ id: existingAgentId });
+ expect(agentInDb.stateful_code_sessions).toBe(false);
+ expect(agentInDb.code_environment_id).toBe('removed-vm');
+ });
+
+ test('restores the deployment-default code environment', async () => {
+ await Agent.updateOne(
+ { id: existingAgentId },
+ {
+ stateful_code_sessions: true,
+ code_environment_id: 'attached-vm',
+ },
+ );
+ mockReq.user.id = existingAgentAuthorId.toString();
+ mockReq.params.id = existingAgentId;
+ mockReq.config = {
+ endpoints: {
+ agents: {
+ statefulCodeSessions: {
+ allowedEnvironments: ['user'],
+ environments: [
+ {
+ id: 'attached-vm',
+ name: 'Attached VM',
+ type: 'attached',
+ baseURL: 'https://bridge.example.com/v1',
+ default: true,
+ },
+ ],
+ },
+ },
+ },
+ };
+ mockReq.body = { code_environment_id: null };
+
+ await updateAgentHandler(mockReq, mockRes);
+
+ expect(mockRes.status).not.toHaveBeenCalledWith(400);
+ const agentInDb = await Agent.findOne({ id: existingAgentId });
+ expect(agentInDb.code_environment_id).toBeUndefined();
+ });
+
test('allows unrelated edits to an existing scope after policy is tightened', async () => {
await Agent.updateOne(
{ id: existingAgentId },
diff --git a/api/server/routes/files/files.js b/api/server/routes/files/files.js
index fd1188a37b..abb2713e92 100644
--- a/api/server/routes/files/files.js
+++ b/api/server/routes/files/files.js
@@ -13,6 +13,7 @@ const {
sendUploadPolicyError,
resolveUploadErrorMessage,
verifyAgentUploadPermission,
+ createCodeExecutionRouteKey,
getCodeExecutionBaseUrl,
assertUploadContentAllowed,
hasActiveFilePolicy,
@@ -350,7 +351,29 @@ router.get('/code/download/:session_id/:fileId', async (req, res) => {
return res.status(400).send('Bad request');
}
const executionProfile = requestedProfile ?? 'default';
- const baseUrl = getCodeExecutionBaseUrl(executionProfile);
+ const requestedRouteKey = req.query.execution_route_key;
+ if (
+ requestedRouteKey != null &&
+ (typeof requestedRouteKey !== 'string' ||
+ executionProfile !== 'stateful' ||
+ !/^stateful:[a-f0-9]{32}$/.test(requestedRouteKey))
+ ) {
+ logger.debug(`${logPrefix} invalid execution_route_key`);
+ return res.status(400).send('Bad request');
+ }
+ const environments =
+ req.config?.endpoints?.[EModelEndpoint.agents]?.statefulCodeSessions?.environments;
+ const configuredEnvironment = requestedRouteKey
+ ? environments?.find(
+ (environment) =>
+ createCodeExecutionRouteKey('stateful', environment) === requestedRouteKey,
+ )
+ : undefined;
+ if (requestedRouteKey && !configuredEnvironment) {
+ logger.debug(`${logPrefix} unknown execution_route_key`);
+ return res.status(404).send('Not found');
+ }
+ const baseUrl = getCodeExecutionBaseUrl(executionProfile, configuredEnvironment);
const { getDownloadStream } = getStrategyFunctions(FileSources.execute_code);
if (!getDownloadStream) {
diff --git a/api/server/routes/files/files.test.js b/api/server/routes/files/files.test.js
index 34894e2a56..af154d0fee 100644
--- a/api/server/routes/files/files.test.js
+++ b/api/server/routes/files/files.test.js
@@ -45,11 +45,15 @@ jest.mock('sharp', () =>
jest.mock('@librechat/api', () => ({
...jest.requireActual('@librechat/api'),
refreshS3FileUrls: jest.fn(),
- getCodeExecutionBaseUrl: jest.fn((profile) =>
- profile === 'stateful'
- ? process.env.LIBRECHAT_CODE_BASEURL_STATEFUL
- : 'https://code-default.example.com/v1',
- ),
+ getCodeExecutionBaseUrl: jest.fn((profile, environment) => {
+ if (environment?.baseURL) {
+ return environment.baseURL;
+ }
+ if (profile === 'stateful') {
+ return process.env.LIBRECHAT_CODE_BASEURL_STATEFUL;
+ }
+ return 'https://code-default.example.com/v1';
+ }),
}));
jest.mock('~/cache', () => ({
@@ -69,6 +73,7 @@ jest.mock('~/config', () => ({
const { processDeleteRequest } = require('~/server/services/Files/process');
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
+const { createCodeExecutionRouteKey } = require('@librechat/api');
// Import the router after mocks
const router = require('./files');
@@ -84,6 +89,7 @@ describe('File Routes - Delete with Agent Access', () => {
let AclEntry;
let User;
let methods;
+ let requestConfig;
let modelsToCleanup = [];
beforeAll(async () => {
@@ -121,6 +127,7 @@ describe('File Routes - Delete with Agent Access', () => {
id: otherUserId?.toString() || 'default-user',
role: SystemRoles.USER,
};
+ req.config = requestConfig;
req.app.locals = {};
next();
});
@@ -148,6 +155,7 @@ describe('File Routes - Delete with Agent Access', () => {
beforeEach(async () => {
jest.clearAllMocks();
+ requestConfig = {};
// Clear database - clean up all test data
await File.deleteMany({});
@@ -1249,6 +1257,43 @@ describe('File Routes - Delete with Agent Access', () => {
});
describe('GET /files/code/download/:session_id/:fileId', () => {
+ it('resolves a configured environment route for a persisted fallback', async () => {
+ const environment = {
+ id: 'managed-vm',
+ name: 'Managed VM',
+ type: 'managed',
+ baseURL: 'https://managed-code.example.com/v1',
+ default: true,
+ owner: 'deployment',
+ };
+ requestConfig = {
+ endpoints: {
+ agents: {
+ statefulCodeSessions: { environments: [environment] },
+ },
+ },
+ };
+ const executionRouteKey = createCodeExecutionRouteKey('stateful', environment);
+ const getDownloadStream = jest.fn().mockResolvedValue({
+ data: Readable.from(['configured output']),
+ });
+ getStrategyFunctions.mockReturnValue({ getDownloadStream });
+ const sessionId = 's'.repeat(21);
+ const codeFileId = 'f'.repeat(21);
+
+ const response = await request(app).get(
+ `/files/code/download/${sessionId}/${codeFileId}?execution_profile=stateful&execution_route_key=${encodeURIComponent(executionRouteKey)}`,
+ );
+
+ expect(response.status).toBe(200);
+ expect(getDownloadStream).toHaveBeenCalledWith(
+ `${sessionId}/${codeFileId}`,
+ { kind: 'user', id: otherUserId.toString() },
+ expect.any(Object),
+ { baseUrl: environment.baseURL, executionProfile: 'stateful' },
+ );
+ });
+
it('routes a persisted stateful fallback through the stateful Code API', async () => {
const getDownloadStream = jest.fn().mockResolvedValue({
headers: {
@@ -1285,6 +1330,15 @@ describe('File Routes - Delete with Agent Access', () => {
}
});
+ it('rejects an unmapped configured-environment route before contacting Code API', async () => {
+ const response = await request(app).get(
+ `/files/code/download/${'s'.repeat(21)}/${'f'.repeat(21)}?execution_profile=stateful&execution_route_key=stateful:${'a'.repeat(32)}`,
+ );
+
+ expect(response.status).toBe(404);
+ expect(getStrategyFunctions).not.toHaveBeenCalled();
+ });
+
it('rejects an unknown execution profile', async () => {
const response = await request(app).get(
`/files/code/download/${'s'.repeat(21)}/${'f'.repeat(21)}?execution_profile=attacker`,
diff --git a/api/server/services/Endpoints/agents/initialize.js b/api/server/services/Endpoints/agents/initialize.js
index fa4d6710f0..024246b80b 100644
--- a/api/server/services/Endpoints/agents/initialize.js
+++ b/api/server/services/Endpoints/agents/initialize.js
@@ -21,6 +21,7 @@ const {
buildAgentContextAttachmentsByAgentId,
collectCodeExecutionProfileRoutes,
getLazySubagentConfigId,
+ resolveCodeExecutionContext,
createStatefulCodeEnvironmentPolicyError,
buildSubagentThreadTaskConfig,
backgroundCompletionWakeupsEnabled,
@@ -921,9 +922,11 @@ const initializeClient = async ({
};
const toLazySubagentMetadata = async (agent) => {
+ const lazyCodeEnvAvailable =
+ codeEnvAvailable === true && agent.tools?.includes(Tools.execute_code) === true;
const statefulCodeSessions =
statefulSessionsAvailable === true &&
- codeEnvAvailable === true &&
+ lazyCodeEnvAvailable &&
agent.stateful_code_sessions === true &&
agent.tools?.includes(Tools.execute_code) === true;
const statefulCodeEnvironment = agent.stateful_code_environment ?? 'user';
@@ -933,6 +936,23 @@ const initializeClient = async ({
) {
throw createStatefulCodeEnvironmentPolicyError(statefulCodeEnvironment);
}
+ const configuredCodeEnvironments =
+ appConfig?.endpoints?.[EModelEndpoint.agents]?.statefulCodeSessions?.environments;
+ const hasConfiguredCodeEnvironment =
+ agent.code_environment_id != null ||
+ configuredCodeEnvironments?.some((environment) => environment.default === true) === true;
+ const codeExecutionContext =
+ lazyCodeEnvAvailable && (!statefulCodeSessions || hasConfiguredCodeEnvironment)
+ ? resolveCodeExecutionContext({
+ statefulSessions: statefulCodeSessions,
+ environment: statefulCodeEnvironment,
+ environmentId: agent.code_environment_id,
+ environments: configuredCodeEnvironments,
+ userId,
+ agentId: agent.id,
+ conversationId,
+ })
+ : undefined;
return {
id: agent.id,
name: agent.name,
@@ -946,10 +966,11 @@ const initializeClient = async ({
memoryAvailable === true && agent.tools?.includes(Tools.memory) === true,
subagents: agent.subagents,
configId: getLazySubagentConfigId(agent),
- codeEnvAvailable:
- codeEnvAvailable === true && agent.tools?.includes(Tools.execute_code) === true,
+ codeEnvAvailable: lazyCodeEnvAvailable,
statefulCodeSessions,
statefulCodeEnvironment,
+ codeExecutionContext,
+ codeSessionKey: codeExecutionContext?.codeSessionKey,
includeReasoningHistory: getIncludeReasoningHistory(agent),
alwaysApplySkillPrimes: await resolveLazyAlwaysApplySkillPrimes(agent),
};
@@ -1182,6 +1203,8 @@ const initializeClient = async ({
codeEnvAvailable: metadata.codeEnvAvailable,
statefulCodeSessions: metadata.statefulCodeSessions,
statefulCodeEnvironment: metadata.statefulCodeEnvironment,
+ codeExecutionContext: metadata.codeExecutionContext,
+ codeSessionKey: metadata.codeSessionKey,
includeReasoningHistory: metadata.includeReasoningHistory,
alwaysApplySkillPrimes: metadata.alwaysApplySkillPrimes,
lazySubagentConfigs: lazyChildren,
diff --git a/api/server/services/Endpoints/agents/initialize.spec.js b/api/server/services/Endpoints/agents/initialize.spec.js
index 48fc331389..7251d7d980 100644
--- a/api/server/services/Endpoints/agents/initialize.spec.js
+++ b/api/server/services/Endpoints/agents/initialize.spec.js
@@ -1204,6 +1204,60 @@ describe('initializeClient — subagent loading', () => {
expect(mockInitializeAgent).toHaveBeenCalledTimes(1);
});
+ it('retains a configured Code API route on lazy subagent descriptors', async () => {
+ const subAgent = await createAgent({
+ id: SUBAGENT_ID,
+ name: 'Attached Stateful Subagent',
+ provider: 'openai',
+ model: 'gpt-4',
+ author: new mongoose.Types.ObjectId(),
+ tools: ['execute_code'],
+ stateful_code_sessions: true,
+ stateful_code_environment: 'agent-user',
+ code_environment_id: 'attached-vm',
+ });
+ 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: ['agent-user'],
+ environments: [
+ {
+ id: 'attached-vm',
+ name: 'Attached VM',
+ type: 'attached',
+ baseURL: 'https://bridge.example.com/v1/',
+ default: true,
+ },
+ ],
+ };
+
+ await initializeClient({
+ req,
+ res: {},
+ signal: new AbortController().signal,
+ endpointOption: makeEndpointOption(),
+ });
+
+ expect(agentClientArgs.agent.lazySubagentConfigs[0]).toEqual(
+ expect.objectContaining({
+ codeSessionKey: expect.stringMatching(/^execute_code:stateful:[a-f0-9]{32}:v3:/),
+ codeExecutionContext: expect.objectContaining({
+ baseUrl: 'https://bridge.example.com/v1',
+ environmentId: 'attached-vm',
+ environmentType: 'attached',
+ executionProfile: 'stateful',
+ executionRouteKey: expect.stringMatching(/^stateful:[a-f0-9]{32}$/),
+ }),
+ }),
+ );
+ });
+
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] },
diff --git a/api/server/services/Files/Code/crud.js b/api/server/services/Files/Code/crud.js
index f04e5ff682..27986ca215 100644
--- a/api/server/services/Files/Code/crud.js
+++ b/api/server/services/Files/Code/crud.js
@@ -1,7 +1,7 @@
const FormData = require('form-data');
const { logger } = require('@librechat/data-schemas');
const { getCodeBaseURL } = require('@librechat/agents');
-const { getCodeEnvRefs } = require('librechat-data-provider');
+const { EModelEndpoint, getCodeEnvRefs } = require('librechat-data-provider');
const {
logAxiosError,
appendCodeEnvFile,
@@ -12,6 +12,7 @@ const {
buildCodeEnvDownloadQuery,
getCodeApiAuthHeaders,
getCodeExecutionBaseUrl,
+ createCodeExecutionRouteKey,
CODE_API_EXPECTED_PROFILE_HEADER,
} = require('@librechat/api');
@@ -82,8 +83,40 @@ async function deleteCodeEnvFile(req, file) {
const missingOrUnsupportedStatuses = new Set([404, 405]);
const authHeaders = await getCodeApiAuthHeaders(req);
- for (const [executionProfile, ref] of refs) {
- const baseURL = getCodeExecutionBaseUrl(executionProfile);
+ for (const [executionRouteKey, ref] of refs) {
+ const executionProfile = ref.executionProfile ?? 'default';
+ const environments =
+ req.config?.endpoints?.[EModelEndpoint.agents]?.statefulCodeSessions?.environments;
+ const configuredEnvironment = environments?.find(
+ (environment) =>
+ createCodeExecutionRouteKey(executionProfile, environment) === executionRouteKey,
+ );
+ if (
+ executionProfile === 'stateful' &&
+ executionRouteKey !== executionProfile &&
+ !configuredEnvironment
+ ) {
+ logger.warn(
+ `[deleteCodeEnvFile] Skipping remote cleanup for unmapped historical route ${executionRouteKey}`,
+ );
+ continue;
+ }
+ let baseURL;
+ try {
+ baseURL = getCodeExecutionBaseUrl(executionProfile, configuredEnvironment);
+ } catch (error) {
+ if (
+ executionProfile === 'stateful' &&
+ executionRouteKey === executionProfile &&
+ !configuredEnvironment
+ ) {
+ logger.warn(
+ '[deleteCodeEnvFile] Skipping remote cleanup for retired legacy stateful route',
+ );
+ continue;
+ }
+ throw error;
+ }
const query = buildCodeEnvDownloadQuery({
kind: ref.kind,
id: ref.id,
diff --git a/api/server/services/Files/Code/crud.spec.js b/api/server/services/Files/Code/crud.spec.js
index 7cc9e743fd..c4a5dec457 100644
--- a/api/server/services/Files/Code/crud.spec.js
+++ b/api/server/services/Files/Code/crud.spec.js
@@ -64,6 +64,7 @@ const {
codeServerHttpAgent,
codeServerHttpsAgent,
getCodeApiAuthHeaders,
+ getCodeExecutionBaseUrl,
} = require('@librechat/api');
const { deleteCodeEnvFile, getCodeOutputDownloadStream, uploadCodeEnvFile } = require('./crud');
@@ -269,6 +270,43 @@ describe('Code CRUD', () => {
);
});
+ it('skips remote cleanup instead of falling back when a historical route is unmapped', async () => {
+ const historicalRoute = 'stateful:0123456789abcdef0123456789abcdef';
+ const historicalFile = {
+ metadata: {
+ codeEnvRefs: {
+ [historicalRoute]: {
+ ...file.metadata.codeEnvRef,
+ executionProfile: 'stateful',
+ executionRouteKey: historicalRoute,
+ },
+ },
+ },
+ };
+
+ await expect(deleteCodeEnvFile(req, historicalFile)).resolves.toBeUndefined();
+
+ expect(mockAxios).not.toHaveBeenCalled();
+ });
+
+ it('skips legacy stateful cleanup after its endpoint is retired', async () => {
+ getCodeExecutionBaseUrl.mockImplementationOnce(() => {
+ throw new Error('LIBRECHAT_CODE_BASEURL_STATEFUL is not configured');
+ });
+ const legacyStatefulFile = {
+ metadata: {
+ codeEnvRef: {
+ ...file.metadata.codeEnvRef,
+ executionProfile: 'stateful',
+ },
+ },
+ };
+
+ await expect(deleteCodeEnvFile(req, legacyStatefulFile)).resolves.toBeUndefined();
+
+ expect(mockAxios).not.toHaveBeenCalled();
+ });
+
it.each([404, 405])(
'falls back to the legacy code environment delete route after a %s',
async (status) => {
diff --git a/api/server/services/Files/Code/process.js b/api/server/services/Files/Code/process.js
index 9c782b0ff3..6bb352e011 100644
--- a/api/server/services/Files/Code/process.js
+++ b/api/server/services/Files/Code/process.js
@@ -229,6 +229,7 @@ const prepareCodeOutputForInspection = async ({
* @param {string} params.messageId - The current message ID.
* @param {number} params.expiresAt - Expiration timestamp (24 hours from creation).
* @param {'default'|'stateful'} [params.executionProfile] - Code API route for later fallback download.
+ * @param {string} [params.executionRouteKey] - Deployment-local route identity.
* @returns {Object} Fallback response with download URL.
*/
const createDownloadFallback = ({
@@ -241,12 +242,20 @@ const createDownloadFallback = ({
toolCallId,
conversationId,
executionProfile,
+ executionRouteKey,
}) => {
const basePath = getBasePath();
- const profileQuery = executionProfile === 'stateful' ? '?execution_profile=stateful' : '';
+ const query = new URLSearchParams();
+ if (executionProfile === 'stateful') {
+ query.set('execution_profile', 'stateful');
+ }
+ if (executionRouteKey && executionRouteKey !== executionProfile) {
+ query.set('execution_route_key', executionRouteKey);
+ }
+ const routeQuery = query.size > 0 ? `?${query.toString()}` : '';
return {
filename: name,
- filepath: `${basePath}/api/files/code/download/${session_id}/${id}${profileQuery}`,
+ filepath: `${basePath}/api/files/code/download/${session_id}/${id}${routeQuery}`,
expiresAt,
conversationId,
toolCallId,
@@ -494,6 +503,7 @@ const runPreviewFinalize = ({ finalize, fileId, previewRevision, onResolved }) =
* @param {string} params.messageId - The current message ID.
* @param {string} [params.codeApiBaseUrl] - Trusted per-agent Code API endpoint.
* @param {'default'|'stateful'} [params.executionProfile] - Trusted execution profile.
+ * @param {string} [params.executionRouteKey] - Trusted deployment-local route identity.
* @param {Buffer} [params.preparedBuffer] - Bytes downloaded during a
* no-write content inspection preflight.
* @param {boolean} [params.downloadFallback] - Return the bounded download
@@ -512,6 +522,7 @@ const processCodeOutput = async ({
freshClaimAfter,
codeApiBaseUrl,
executionProfile = 'default',
+ executionRouteKey = executionProfile,
preparedBuffer,
downloadFallback,
}) => {
@@ -535,6 +546,7 @@ const processCodeOutput = async ({
session_id,
conversationId,
executionProfile,
+ executionRouteKey,
expiresAt: currentDate.getTime() + 86400000,
}),
};
@@ -565,6 +577,7 @@ const processCodeOutput = async ({
session_id,
conversationId,
executionProfile,
+ executionRouteKey,
expiresAt: currentDate.getTime() + 86400000,
}),
};
@@ -579,6 +592,7 @@ const processCodeOutput = async ({
storage_session_id: session_id,
file_id: id,
executionProfile,
+ ...(executionRouteKey !== executionProfile ? { executionRouteKey } : {}),
};
/* `safeName` keeps the directory structure (`a/b/file.txt` -> `a/b/file.txt`)
@@ -752,6 +766,7 @@ const processCodeOutput = async ({
session_id,
conversationId,
executionProfile,
+ executionRouteKey,
expiresAt: currentDate.getTime() + 86400000,
}),
};
@@ -935,6 +950,7 @@ const processCodeOutput = async ({
session_id,
conversationId,
executionProfile,
+ executionRouteKey,
expiresAt: currentDate.getTime() + 86400000,
}),
};
@@ -1102,6 +1118,7 @@ const primeFiles = async (options) => {
agentResourceType,
codeApiBaseUrl,
executionProfile = 'default',
+ executionRouteKey = executionProfile,
} = options;
const codeApiRoute = { baseUrl: codeApiBaseUrl, executionProfile };
const file_ids = tool_resources?.[EToolResources.execute_code]?.file_ids ?? [];
@@ -1160,7 +1177,7 @@ const primeFiles = async (options) => {
continue;
}
- const ref = getCodeEnvRefForProfile(file.metadata, executionProfile);
+ const ref = getCodeEnvRefForProfile(file.metadata, executionRouteKey);
const sourceRef = ref ?? getCodeEnvRefs(file.metadata)[0]?.[1];
if (!sourceRef) {
skippedNoRef += 1;
@@ -1250,6 +1267,7 @@ const primeFiles = async (options) => {
storage_session_id: uploaded.storage_session_id,
file_id: uploaded.file_id,
executionProfile,
+ ...(executionRouteKey !== executionProfile ? { executionRouteKey } : {}),
...(sourceRef.kind === 'skill' ? { version: sourceRef.version } : {}),
};
@@ -1258,7 +1276,7 @@ const primeFiles = async (options) => {
await updateFile({
file_id: file.file_id,
'metadata.codeEnvRef': updatedRefs.codeEnvRef,
- [`metadata.codeEnvRefs.${executionProfile}`]: newRef,
+ [`metadata.codeEnvRefs.${executionRouteKey}`]: newRef,
});
sessions.set(newRef.storage_session_id, true);
pushFile(newRef.storage_session_id, newRef.file_id);
diff --git a/api/server/services/Files/Code/process.spec.js b/api/server/services/Files/Code/process.spec.js
index 52e99ced88..8e2b3b258a 100644
--- a/api/server/services/Files/Code/process.spec.js
+++ b/api/server/services/Files/Code/process.spec.js
@@ -978,11 +978,13 @@ describe('Code Process', () => {
describe('fallback behavior', () => {
it('preserves the stateful route in generated downloads and fallbacks', async () => {
mockAxios.mockRejectedValue(new Error('Network error'));
+ const executionRouteKey = `stateful:${'a'.repeat(32)}`;
const { file: result } = await processCodeOutput({
...baseParams,
codeApiBaseUrl: 'https://code-stateful.example.com',
executionProfile: 'stateful',
+ executionRouteKey,
});
expect(mockAxios).toHaveBeenCalledWith(
@@ -992,6 +994,9 @@ describe('Code Process', () => {
}),
);
expect(result.filepath).toContain('execution_profile=stateful');
+ expect(result.filepath).toContain(
+ `execution_route_key=${encodeURIComponent(executionRouteKey)}`,
+ );
});
it('should fallback to download URL when saveBuffer is not available', async () => {
@@ -1091,14 +1096,19 @@ describe('Code Process', () => {
it('persists the originating profile on a stateful artifact ref', async () => {
mockAxios.mockResolvedValue({ data: Buffer.alloc(100) });
+ const executionRouteKey = `stateful:${'a'.repeat(32)}`;
const { file: result } = await processCodeOutput({
...baseParams,
codeApiBaseUrl: 'https://code-stateful.example.com',
executionProfile: 'stateful',
+ executionRouteKey,
});
- expect(result.metadata.codeEnvRef.executionProfile).toBe('stateful');
+ expect(result.metadata.codeEnvRef).toEqual(
+ expect.objectContaining({ executionProfile: 'stateful', executionRouteKey }),
+ );
+ expect(result.metadata.codeEnvRefs[executionRouteKey]).toEqual(result.metadata.codeEnvRef);
});
/* Phase C lock-in: outputs are ALWAYS user-scoped, never skill-scoped.
diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js
index 200f81fbd0..f5655088a8 100644
--- a/api/server/services/ToolService.js
+++ b/api/server/services/ToolService.js
@@ -785,6 +785,8 @@ async function loadToolDefinitionsWrapper({
enabledCapabilities.has(AgentCapabilities.stateful_code_sessions) &&
agent.stateful_code_sessions === true,
environment: agent.stateful_code_environment,
+ environmentId: agent.code_environment_id,
+ environments: req.config?.endpoints?.agents?.statefulCodeSessions?.environments,
userId: req.user.id,
agentId: agent.id,
conversationId: runtimeRequestBody?.conversationId,
@@ -1392,6 +1394,7 @@ async function loadToolDefinitionsWrapper({
agentResourceType,
codeApiBaseUrl: resolvedCodeExecutionContext.baseUrl,
executionProfile: resolvedCodeExecutionContext.executionProfile,
+ executionRouteKey: resolvedCodeExecutionContext.executionRouteKey,
});
if (toolContext) {
dynamicToolContextMap[Tools.execute_code] = toolContext;
@@ -1642,6 +1645,8 @@ async function loadAgentTools({
resolveCodeExecutionContext({
statefulSessions: statefulCodeSessions,
environment: agent.stateful_code_environment,
+ environmentId: agent.code_environment_id,
+ environments: req.config?.endpoints?.agents?.statefulCodeSessions?.environments,
userId: req.user.id,
agentId: agent.id,
conversationId: requestBody?.conversationId ?? req.body?.conversationId,
@@ -2022,6 +2027,8 @@ async function loadToolsForExecution({
const codeExecutionContext = resolveCodeExecutionContext({
statefulSessions: statefulCodeSessions,
environment: agent?.stateful_code_environment,
+ environmentId: agent?.code_environment_id,
+ environments: req.config?.endpoints?.agents?.statefulCodeSessions?.environments,
userId: req.user.id,
agentId: agent?.id,
conversationId: conversationId ?? runtimeRequestBody?.conversationId,
diff --git a/client/src/common/agents-types.ts b/client/src/common/agents-types.ts
index c5ac569ac2..1500d9e503 100644
--- a/client/src/common/agents-types.ts
+++ b/client/src/common/agents-types.ts
@@ -47,6 +47,8 @@ export type AgentForm = {
memory_scope?: MemoryScope;
/** Sharing scope for stateful Code API workspaces. */
stateful_code_environment?: StatefulCodeEnvironment;
+ /** Operator-configured managed or attached execution environment. */
+ code_environment_id?: string | null;
provider?: AgentProvider | OptionWithIcon;
/** @deprecated Use edges instead */
agent_ids?: string[];
diff --git a/client/src/components/SidePanel/Agents/Advanced/StatefulSessions.tsx b/client/src/components/SidePanel/Agents/Advanced/StatefulSessions.tsx
index 77d6445e59..d0958c0a63 100644
--- a/client/src/components/SidePanel/Agents/Advanced/StatefulSessions.tsx
+++ b/client/src/components/SidePanel/Agents/Advanced/StatefulSessions.tsx
@@ -29,6 +29,8 @@ const ENVIRONMENT_LABELS = {
conversation: 'com_ui_stateful_code_environment_conversation',
} as const;
+const DEPLOYMENT_DEFAULT_ENVIRONMENT = '__deployment_default__';
+
export default function StatefulSessions() {
const localize = useLocalize();
const { user } = useAuthContext();
@@ -39,7 +41,9 @@ export default function StatefulSessions() {
const enabled = watch(AgentCapabilities.stateful_code_sessions) ?? false;
const codeEnabled = watch(AgentCapabilities.execute_code);
const environment = watch('stateful_code_environment') ?? 'user';
+ const codeEnvironmentId = watch('code_environment_id');
const configuredEnvironments = agentsConfig?.statefulCodeSessions?.allowedEnvironments;
+ const executionEnvironments = agentsConfig?.statefulCodeSessions?.environments ?? [];
const allowedEnvironments = resolveAllowedStatefulCodeEnvironments(configuredEnvironments);
const handleChange = (value: boolean) => {
@@ -91,6 +95,46 @@ export default function StatefulSessions() {
{enabled && codeEnabled === true && (
+ {executionEnvironments.length > 0 && (
+ <>
+
+
+
+ {localize('com_nav_info_code_environment')}
+
+ >
+ )}