🗿 feat: Add Attached Stateful Code Environments (#15352)

* feat: add attached stateful code environments

* fix: include code environment in lazy agent type

* fix: harden stateful environment routing

* fix: complete code environment route isolation

* fix: declare code environment map type

* fix: preserve configured code execution routes

* fix: harden stateful environment updates

* test: assert route-scoped sandbox readiness

* fix: harden stateful environment lifecycle

* fix: isolate migrated code sessions

* test: assert route-qualified code sessions
This commit is contained in:
Danny Avila 2026-08-30 15:38:45 -04:00 • committed by GitHub
parent dc77b78d3e
commit c1cb591d49
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
59 changed files with 1433 additions and 103 deletions

View file

@ -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;

View file

@ -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);

View file

@ -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,
});

View file

@ -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,

View file

@ -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;

View file

@ -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 },

View file

@ -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) {

View file

@ -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`,

View file

@ -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,

View file

@ -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] },

View file

@ -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,

View file

@ -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) => {

View file

@ -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);

View file

@ -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.

View file

@ -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,

View file

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

View file

@ -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() {
</HoverCard>
{enabled && codeEnabled === true && (
<div className="space-y-2 pl-1">
{executionEnvironments.length > 0 && (
<>
<label
className="text-xs font-medium text-text-secondary"
htmlFor="code-environment-id"
>
{localize('com_ui_code_environment')}
</label>
<Select
value={codeEnvironmentId ?? DEPLOYMENT_DEFAULT_ENVIRONMENT}
onValueChange={(value) => {
setValue(
'code_environment_id',
value === DEPLOYMENT_DEFAULT_ENVIRONMENT ? null : value,
{ shouldDirty: true },
);
}}
>
<SelectTrigger id="code-environment-id" data-testid="code-environment-id">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value={DEPLOYMENT_DEFAULT_ENVIRONMENT}>
{localize('com_ui_code_environment_deployment_default')}
</SelectItem>
{executionEnvironments.map((candidate) => (
<SelectItem key={candidate.id} value={candidate.id}>
{candidate.name}
{candidate.type === 'attached'
? ` (${localize('com_ui_code_environment_attached')})`
: ''}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-text-tertiary">
{localize('com_nav_info_code_environment')}
</p>
</>
)}
<label
className="text-xs font-medium text-text-secondary"
htmlFor="stateful-code-environment"

View file

@ -84,6 +84,7 @@ export function composeAgentUpdatePayload(data: AgentForm, agent_id?: string | n
hide_sequential_outputs,
stateful_code_sessions,
stateful_code_environment,
code_environment_id,
recursion_limit,
category,
support_contact,
@ -124,6 +125,7 @@ export function composeAgentUpdatePayload(data: AgentForm, agent_id?: string | n
hide_sequential_outputs,
stateful_code_sessions: normalizedStatefulCodeSessions,
stateful_code_environment: normalizedStatefulCodeEnvironment,
code_environment_id: agent_id ? code_environment_id : (code_environment_id ?? undefined),
recursion_limit,
category,
support_contact,

View file

@ -100,6 +100,7 @@ function AgentSelect({
avatar_preview: fullAgent.avatar?.filepath ?? '',
avatar_action: null,
stateful_code_environment: fullAgent.stateful_code_environment ?? 'user',
code_environment_id: fullAgent.code_environment_id,
};
Object.entries(fullAgent).forEach(([name, value]) => {

View file

@ -141,6 +141,24 @@ describe('composeAgentUpdatePayload', () => {
expect(payload.stateful_code_environment).toBe('agent-user');
});
it('sends a deployment-default reset for an existing agent', () => {
const form = createForm();
form.code_environment_id = null;
const { payload } = composeAgentUpdatePayload(form, 'agent_123');
expect(payload.code_environment_id).toBeNull();
});
it('omits a deployment-default reset when creating an agent', () => {
const form = createForm();
form.code_environment_id = null;
const { payload } = composeAgentUpdatePayload(form);
expect(payload.code_environment_id).toBeUndefined();
});
});
describe('persistAvatarChanges', () => {

View file

@ -2168,6 +2168,10 @@
"com_ui_standard": "Standard",
"com_ui_stateful_sessions": "Stateful code sessions",
"com_ui_stateful_code_environment": "Stateful environment",
"com_ui_code_environment": "Execution environment",
"com_ui_code_environment_attached": "attached VM",
"com_ui_code_environment_deployment_default": "Deployment default",
"com_nav_info_code_environment": "Choose the managed runtime or attached VM that owns this workspace.",
"com_ui_stateful_code_environment_user": "User workspace (recommended)",
"com_ui_stateful_code_environment_agent_user": "Agent + user workspace",
"com_ui_stateful_code_environment_conversation": "Conversation workspace",

View file

@ -79,6 +79,7 @@ endpoints:
- chain
- ocr
- run_in_background
# __E2E_CODE_BRIDGE_CONFIG__
# Keep the shared mock profile non-interactive except for the dedicated
# approval probe. This exercises real HITL pause/resume without wedging the
# existing file-authoring, steering, background-tool, or MCP specs.

View file

@ -209,6 +209,23 @@ function writeRuntimeMockConfig() {
.replace('# __E2E_DYNAMIC_MCP_ALLOWED_DOMAIN__', dynamicMcpConfig.allowedDomain)
.replace('# __E2E_DYNAMIC_MCP_STDIO_ENV__', dynamicMcpConfig.stdioEnv)
.replace('# __E2E_DYNAMIC_MCP_NETWORK_SERVERS__', dynamicMcpConfig.networkServers);
const codeBridgeURL = process.env.E2E_CODE_BRIDGE_URL;
config = config.replace(
'# __E2E_CODE_BRIDGE_CONFIG__',
codeBridgeURL
? [
' - stateful_code_sessions',
'statefulCodeSessions:',
' allowedEnvironments: ["conversation"]',
' environments:',
' - id: e2e-vm',
' name: E2E attached VM',
' type: attached',
` baseURL: ${JSON.stringify(codeBridgeURL)}`,
' default: true',
].join('\n ')
: '# __E2E_CODE_BRIDGE_CONFIG__',
);
/** Keep the generated config in lockstep with the overridable label-server
* port: the template hard-codes 8889, so an `E2E_LABEL_PORT` override that
* moved only the server and its health check would report ready while

View file

@ -41,6 +41,7 @@ const ASK_USER_QUESTION_MARKER = 'E2E_ASK_USER_QUESTION:';
const RESUME_ICON_REPLY_MARKER = 'E2E_RESUME_ICON_REPLY:';
const FORCED_ERROR_MARKER = 'E2E_FORCED_ERROR:';
const MARKDOWN_REPLY_MARKER = 'E2E_MARKDOWN_REPLY';
const STATEFUL_CODE_MARKER = 'E2E_STATEFUL_CODE:';
/** Two prose paragraphs, so a spec can select the message's *closing* block. */
const PARAGRAPHS_REPLY_MARKER = 'E2E_PARAGRAPHS_REPLY';
const MERMAID_ARTIFACT_REPLY_MARKER = 'E2E_MERMAID_ARTIFACT_REPLY';
@ -88,6 +89,7 @@ const RESUME_ICON_REPLY_CHUNKS = 240;
const CREATE_FILE_TOOL_NAME = 'create_file';
const EDIT_FILE_TOOL_NAME = 'edit_file';
const BASH_TOOL_NAME = 'bash_tool';
const STATEFUL_CODE_VALUE = 'librechat-bridge-persisted';
const SKILL_TOOL_NAME = 'skill';
const CREATE_SKILL_TOOL_CALL_ID = 'call_e2e_create_skill';
const EDIT_SKILL_TOOL_CALL_ID = 'call_e2e_edit_skill';
@ -1647,6 +1649,42 @@ function backgroundCompletionResponses(text) {
};
}
function statefulCodeResponses(operation, toolNames) {
if (!toolNames.has(BASH_TOOL_NAME)) {
return {
responses: [`E2E stateful code unavailable: ${BASH_TOOL_NAME} was not advertised.`],
};
}
const commands = {
write: `printf ${STATEFUL_CODE_VALUE} > /mnt/data/librechat-bridge-state.txt && cat /mnt/data/librechat-bridge-state.txt`,
read: 'cat /mnt/data/librechat-bridge-state.txt',
};
const command = commands[operation];
if (!command) {
return { responses: [`E2E stateful code failed: unsupported operation ${operation}`] };
}
return {
responses: ['', ''],
toolCalls: [
{
id: `call_e2e_stateful_code_${operation}`,
name: BASH_TOOL_NAME,
args: { command },
type: 'tool_call',
},
],
resolveOnStream: (streamMessages) => {
const toolText = findLastToolMessageText(streamMessages, STATEFUL_CODE_VALUE);
if (!toolText) {
return null;
}
return { responses: [`E2E stateful code ${operation} observed ${STATEFUL_CODE_VALUE}`] };
},
};
}
function parseHandoffScript(text) {
const encodedScript = getMarkerValue(text, HANDOFF_MARKER);
if (!encodedScript) {
@ -2274,6 +2312,11 @@ function resolveResponses({ graph, messages, text, toolNames }) {
return reply;
}
const statefulCodeOperation = getMarkerValue(text, STATEFUL_CODE_MARKER);
if (statefulCodeOperation) {
return statefulCodeResponses(statefulCodeOperation, toolNames);
}
const steerToolLabel = getMarkerValue(text, STEER_TOOL_REPLY_MARKER);
if (steerToolLabel) {
return steerToolReplyResponses(steerToolLabel, toolNames);

View file

@ -0,0 +1,66 @@
import { expect, test } from '@playwright/test';
import type { AgentDetail } from './agents.helpers';
import { cleanupAgent, openAgentBuilder, uniqueAgentName } from './agents.helpers';
import {
MOCK_ENDPOINTS,
NEW_CHAT_PATH,
getAccessToken,
messagesView,
requestJson,
sendMessageAndWaitForCompletion,
} from './helpers';
const CODE_VALUE = 'librechat-bridge-persisted';
test.describe('attached stateful code environment', () => {
test.skip(!process.env.E2E_CODE_BRIDGE_URL, 'E2E_CODE_BRIDGE_URL is required');
test('routes two conversation turns through the bridge and preserves workspace state', async ({
page,
}) => {
test.setTimeout(120000);
await page.goto(NEW_CHAT_PATH, { timeout: 15000 });
const name = uniqueAgentName('E2E Attached Code Agent');
let agentId: string | undefined;
try {
const token = await getAccessToken(page);
const agent = await requestJson<AgentDetail>(page, {
path: '/api/agents',
token,
method: 'POST',
body: {
name,
description: 'Exercises the outbound stateful code bridge.',
instructions: 'Use the requested code tool and report its output.',
provider: MOCK_ENDPOINTS[0].label,
model: MOCK_ENDPOINTS[0].model,
tools: ['execute_code'],
stateful_code_sessions: true,
stateful_code_environment: 'conversation',
code_environment_id: 'e2e-vm',
},
});
agentId = agent.id;
const form = await openAgentBuilder(page);
await form.getByRole('combobox', { name: 'Agent', exact: true }).click();
await page.getByRole('option', { name }).click();
await expect(form.getByLabel('Agent name')).toHaveValue(name);
await form.getByRole('button', { name: 'Select Agent' }).click();
await sendMessageAndWaitForCompletion(page, 'E2E_STATEFUL_CODE:write');
await expect(
messagesView(page).getByText(`E2E stateful code write observed ${CODE_VALUE}`),
).toBeVisible({ timeout: 30000 });
await sendMessageAndWaitForCompletion(page, 'E2E_STATEFUL_CODE:read');
await expect(
messagesView(page).getByText(`E2E stateful code read observed ${CODE_VALUE}`),
).toBeVisible({ timeout: 30000 });
} finally {
await cleanupAgent(page, agentId);
}
});
});

View file

@ -566,6 +566,18 @@ endpoints:
# # (optional) Limit the workspace scopes users may select. Omit to allow all three.
# statefulCodeSessions:
# allowedEnvironments: ["user", "agent-user", "conversation"]
# # Route an agent to an operator-managed Code API or to a Code API whose
# # `remote-bridge` backend leases work to an outbound @librechat/code worker.
# environments:
# - id: managed-default
# name: Managed Code API
# type: managed
# baseURL: https://code.example.com/v1
# default: true
# - id: engineering-vm
# name: Engineering VM
# type: attached
# baseURL: https://code-bridge.example.com/v1
# # (optional) Trusted origin for internal event delivery. By default, LibreChat
# # uses its own bound listener; override only for an internal TLS/front-door route.
# eventDriven:

View file

@ -565,6 +565,29 @@ describe('collectCodeExecutionProfileRoutes', () => {
expect(routes).toEqual([{ codeExecutionContext: graphContext, codeSessionKeys: [graphKey] }]);
});
it('keeps configured stateful deployments in separate routing namespaces', () => {
const context = (executionRouteKey: string, baseUrl: string) => ({
baseUrl,
codeSessionKey: `execute_code:stateful:${executionRouteKey}`,
executionProfile: 'stateful' as const,
executionRouteKey,
runtimeSessionHint: `v3:${executionRouteKey}:user:scope`,
statefulSessions: true,
});
const first = context('stateful:first', 'https://first.example/v1');
const second = context('stateful:second', 'https://second.example/v1');
const routes = collectCodeExecutionProfileRoutes([
{ codeEnvAvailable: true, codeExecutionContext: first },
{ codeEnvAvailable: true, codeExecutionContext: second },
]);
expect(routes).toEqual([
{ codeExecutionContext: first, codeSessionKeys: [first.codeSessionKey] },
{ codeExecutionContext: second, codeSessionKeys: [second.codeSessionKey] },
]);
});
it('derives and includes the trusted profile for a lazy subagent descriptor', () => {
process.env.LIBRECHAT_CODE_BASEURL_STATEFUL = 'https://stateful.example.com/v1';
const routes = collectCodeExecutionProfileRoutes(

View file

@ -1,7 +1,11 @@
import { Constants } from '@librechat/agents';
import type { FileRefs, CodeEnvFile, ToolSessionMap, CodeSessionContext } from '@librechat/agents';
import type { StatefulCodeEnvironment } from 'librechat-data-provider';
import { resolveCodeExecutionContext, type CodeExecutionContext } from './execution';
import {
getCodeExecutionRouteKey,
resolveCodeExecutionContext,
type CodeExecutionContext,
} from './execution';
/**
* Minimal shape for an agent that may contribute primed code files to its
@ -54,7 +58,7 @@ export function collectCodeExecutionProfileRoutes(
scope?: { userId: string; conversationId?: string | null },
): CodeExecutionProfileRoute[] {
const routes = new Map<
CodeExecutionContext['executionProfile'],
string,
{ codeExecutionContext: CodeExecutionContext; codeSessionKeys: Set<string> }
>();
const visited = new Set<CodeFilesAgent>();
@ -78,12 +82,13 @@ export function collectCodeExecutionProfileRoutes(
})
: undefined);
if (agent.codeEnvAvailable === true && context) {
const route = routes.get(context.executionProfile) ?? {
const routeKey = getCodeExecutionRouteKey(context);
const route = routes.get(routeKey) ?? {
codeExecutionContext: context,
codeSessionKeys: new Set<string>(),
};
route.codeSessionKeys.add(agent.codeSessionKey ?? context.codeSessionKey);
routes.set(context.executionProfile, route);
routes.set(routeKey, route);
}
enqueueCodeFilesChildren(agent, queue, visited);
}

View file

@ -148,6 +148,15 @@ describe('agent context compatibility', () => {
expect(changed.digest).not.toBe(fingerprint.digest);
});
it('invalidates initialized context when the code execution route changes', () => {
const fingerprint = (executionRouteKey: string) =>
createInitializedAgentContextFingerprint({
agents: [{ id: 'agent-1', execution: { executionRouteKey } }],
});
expect(fingerprint('stateful:first').digest).not.toBe(fingerprint('stateful:second').digest);
});
it('invalidates a constant-version deployment Skill when its body changes', () => {
const fingerprint = (body: string) =>
createInitializedAgentContextFingerprint({

View file

@ -94,4 +94,95 @@ describe('resolveCodeExecutionContext', () => {
expect(first.runtimeSessionHint).not.toContain('user-1');
expect(second.runtimeSessionHint).not.toContain('user-2');
});
it('routes an agent to its configured attached environment', () => {
const context = resolveCodeExecutionContext({
statefulSessions: true,
environment: 'agent-user',
environmentId: 'my-vm',
environments: [
{
id: 'managed',
name: 'Managed',
type: 'managed',
baseURL: 'https://managed.example/v1',
},
{
id: 'my-vm',
name: 'My VM',
type: 'attached',
baseURL: 'https://bridge.example/v1/',
},
],
userId: 'user-1',
agentId: 'agent-1',
});
expect(context).toEqual(
expect.objectContaining({
baseUrl: 'https://bridge.example/v1',
environmentId: 'my-vm',
environmentType: 'attached',
executionProfile: 'stateful',
}),
);
expect(context.runtimeSessionHint).toMatch(/^v3:[a-f0-9]{12}:agent-user:/);
expect(context.executionRouteKey).toMatch(/^stateful:[a-f0-9]{32}$/);
});
it('namespaces configured deployments independently of the shared wire profile', () => {
const environment = (id: string, baseURL: string) => ({
id,
name: id,
type: 'attached' as const,
baseURL,
default: true,
});
const first = resolveCodeExecutionContext({
statefulSessions: true,
environments: [environment('first', 'https://first.example/v1')],
userId: 'user-1',
});
const replacement = resolveCodeExecutionContext({
statefulSessions: true,
environments: [environment('first', 'https://replacement.example/v1')],
userId: 'user-1',
});
expect(first.executionProfile).toBe('stateful');
expect(replacement.executionProfile).toBe('stateful');
expect(first.runtimeSessionHint).toBe(replacement.runtimeSessionHint);
expect(first.executionRouteKey).not.toBe(replacement.executionRouteKey);
expect(first.codeSessionKey).not.toBe(replacement.codeSessionKey);
});
it('uses the operator-selected default environment when the agent has no override', () => {
const context = resolveCodeExecutionContext({
statefulSessions: true,
environments: [
{
id: 'default-vm',
name: 'Default VM',
type: 'attached',
baseURL: 'https://bridge.example/v1',
default: true,
},
],
userId: 'user-1',
});
expect(context.environmentId).toBe('default-vm');
expect(context.baseUrl).toBe('https://bridge.example/v1');
});
it('fails closed when an agent references an unknown configured environment', () => {
expect(() =>
resolveCodeExecutionContext({
statefulSessions: true,
environmentId: 'missing-vm',
environments: [],
userId: 'user-1',
}),
).toThrow('Stateful code environment "missing-vm" is not configured');
});
});

View file

@ -1,17 +1,43 @@
import { createHash } from 'node:crypto';
import { Constants, getCodeBaseURL } from '@librechat/agents';
import type { StatefulCodeEnvironment } from 'librechat-data-provider';
import type { StatefulCodeEnvironment, TAgentsEndpoint } from 'librechat-data-provider';
export const CODE_API_EXPECTED_PROFILE_HEADER = 'X-CodeAPI-Expected-Profile';
export type CodeExecutionProfile = 'default' | 'stateful';
export type CodeEnvironmentConfig = NonNullable<
NonNullable<TAgentsEndpoint['statefulCodeSessions']>['environments']
>[number];
export interface CodeExecutionContext {
baseUrl: string;
codeSessionKey: string;
executionProfile: CodeExecutionProfile;
/** Stable server-side namespace for deployment-local file references and
* priming work. Unlike `executionProfile`, this distinguishes configured
* Code API deployments that all use the `stateful` wire profile. */
executionRouteKey?: string;
runtimeSessionHint?: string;
statefulSessions: boolean;
environmentId?: string;
environmentType?: CodeEnvironmentConfig['type'];
}
export function createCodeExecutionRouteKey(
profile: CodeExecutionProfile,
environment?: Pick<CodeEnvironmentConfig, 'id' | 'baseURL'>,
): string {
if (profile === 'default' || environment == null) {
return profile;
}
const identity = JSON.stringify([environment.id, environment.baseURL.trim().replace(/\/+$/, '')]);
return `stateful:${createHash('sha256').update(identity).digest('hex').slice(0, 32)}`;
}
export function getCodeExecutionRouteKey(
context: Pick<CodeExecutionContext, 'executionProfile' | 'executionRouteKey'>,
): string {
return context.executionRouteKey ?? context.executionProfile;
}
export function normalizeStatefulCodeEnvironment(
@ -26,10 +52,16 @@ export function normalizeStatefulCodeEnvironment(
return 'user';
}
export function getCodeExecutionBaseUrl(profile: CodeExecutionProfile): string {
export function getCodeExecutionBaseUrl(
profile: CodeExecutionProfile,
environment?: CodeEnvironmentConfig,
): string {
if (profile === 'default') {
return getCodeBaseURL().replace(/\/+$/, '');
}
if (environment) {
return environment.baseURL.trim().replace(/\/+$/, '');
}
const baseUrl = process.env.LIBRECHAT_CODE_BASEURL_STATEFUL?.trim().replace(/\/+$/, '');
if (baseUrl) {
return baseUrl;
@ -41,31 +73,50 @@ export function getCodeExecutionBaseUrl(profile: CodeExecutionProfile): string {
function resolveRuntimeSessionHint(params: {
environment: StatefulCodeEnvironment;
environmentId?: string;
userId: string;
agentId?: string | null;
conversationId?: string | null;
}): string {
const { environment, userId, agentId, conversationId } = params;
const { environment, environmentId, userId, agentId, conversationId } = params;
const scopeFingerprint = (...parts: string[]): string =>
createHash('sha256').update(JSON.stringify(parts)).digest('hex').slice(0, 32);
const prefix = environmentId ? `v3:${scopeFingerprint(environmentId).slice(0, 12)}` : 'v2';
if (environment === 'agent-user') {
if (!agentId) {
throw new Error('Agent-user code environments require an agent ID.');
}
return `v2:agent-user:${scopeFingerprint(userId, agentId)}`;
return `${prefix}:agent-user:${scopeFingerprint(userId, agentId)}`;
}
if (environment === 'conversation') {
if (!conversationId) {
throw new Error('Conversation code environments require a conversation ID.');
}
return `v2:conversation:${scopeFingerprint(userId, conversationId)}`;
return `${prefix}:conversation:${scopeFingerprint(userId, conversationId)}`;
}
return `v2:user:${scopeFingerprint(userId)}`;
return `${prefix}:user:${scopeFingerprint(userId)}`;
}
function resolveConfiguredEnvironment(params: {
environmentId?: string | null;
environments?: readonly CodeEnvironmentConfig[];
}): CodeEnvironmentConfig | undefined {
const { environmentId, environments } = params;
if (environmentId) {
const configured = environments?.find((environment) => environment.id === environmentId);
if (!configured) {
throw new Error(`Stateful code environment "${environmentId}" is not configured.`);
}
return configured;
}
return environments?.find((environment) => environment.default === true);
}
export function resolveCodeExecutionContext(params: {
statefulSessions: boolean;
environment?: StatefulCodeEnvironment | string | null;
environmentId?: string | null;
environments?: readonly CodeEnvironmentConfig[];
userId?: string | null;
agentId?: string | null;
conversationId?: string | null;
@ -80,21 +131,27 @@ export function resolveCodeExecutionContext(params: {
}
const environment = normalizeStatefulCodeEnvironment(params.environment);
const configuredEnvironment = resolveConfiguredEnvironment(params);
if (!params.userId) {
throw new Error('Stateful code environments require an authenticated user ID.');
}
const runtimeSessionHint = resolveRuntimeSessionHint({
environment,
environmentId: configuredEnvironment?.id,
userId: params.userId,
agentId: params.agentId,
conversationId: params.conversationId,
});
const executionRouteKey = createCodeExecutionRouteKey('stateful', configuredEnvironment);
return {
baseUrl: getCodeExecutionBaseUrl('stateful'),
codeSessionKey: `${Constants.EXECUTE_CODE}:stateful:${runtimeSessionHint}`,
baseUrl: getCodeExecutionBaseUrl('stateful', configuredEnvironment),
codeSessionKey: `${Constants.EXECUTE_CODE}:${executionRouteKey}:${runtimeSessionHint}`,
executionProfile: 'stateful',
...(configuredEnvironment ? { executionRouteKey } : {}),
runtimeSessionHint,
statefulSessions: true,
environmentId: configuredEnvironment?.id,
environmentType: configuredEnvironment?.type,
};
}

View file

@ -4489,7 +4489,7 @@ describe('createToolExecuteHandler', () => {
});
expect(result.status).toBe('success');
expect(markSandboxReady).toHaveBeenCalledWith('v2:user:abc');
expect(markSandboxReady).toHaveBeenCalledWith('v2:user:abc', 'stateful');
expect(markSandboxReady).toHaveBeenCalledWith('conversation-1');
jest.mocked(markSandboxReady).mockClear();

View file

@ -4388,6 +4388,8 @@ export function createToolExecuteHandler(options: ToolExecuteOptions): EventHand
);
const codeExecutionContext = getCodeExecutionContext(mergedConfigurable);
const runtimeSessionHint = codeExecutionContext?.runtimeSessionHint;
const executionRouteKey =
codeExecutionContext?.executionRouteKey ?? codeExecutionContext?.executionProfile;
const sandboxConversationId =
((metadata as Record<string, unknown>)?.thread_id as string | undefined) ??
(mergedConfigurable?.thread_id as string | undefined) ??
@ -4398,7 +4400,7 @@ export function createToolExecuteHandler(options: ToolExecuteOptions): EventHand
)?.conversationId;
const markCodeSandboxWarm = (): void => {
if (runtimeSessionHint) {
void markSandboxReady(runtimeSessionHint);
void markSandboxReady(runtimeSessionHint, executionRouteKey);
}
if (sandboxConversationId) {
void markSandboxReady(sandboxConversationId);

View file

@ -20,6 +20,7 @@ const params = {
baseUrl: 'https://code-stateful.example.com',
codeSessionKey: 'execute_code:stateful:test',
executionProfile: 'stateful' as const,
executionRouteKey: `stateful:${'a'.repeat(32)}`,
statefulSessions: true,
},
};
@ -64,6 +65,7 @@ describe('createBackgroundCodeResultHandler generated-file preflight', () => {
session_id: 'storage-session',
codeApiBaseUrl: 'https://code-stateful.example.com',
executionProfile: 'stateful',
executionRouteKey: `stateful:${'a'.repeat(32)}`,
preparedBuffer,
}),
);

View file

@ -78,6 +78,7 @@ export interface CodeHarvestDeps {
freshClaimAfter?: number;
codeApiBaseUrl?: string;
executionProfile?: CodeExecutionContext['executionProfile'];
executionRouteKey?: string;
preparedBuffer?: Buffer;
downloadFallback?: boolean;
}) => Promise<ProcessedCodeOutput | null>;
@ -282,6 +283,7 @@ export function createBackgroundCodeResultHandler(deps: CodeHarvestDeps): CodeHa
freshClaimAfter,
codeApiBaseUrl: codeExecutionContext?.baseUrl,
executionProfile: codeExecutionContext?.executionProfile,
executionRouteKey: codeExecutionContext?.executionRouteKey,
preparedBuffer,
downloadFallback,
});

View file

@ -973,6 +973,9 @@ export async function initializeAgent(
const codeExecutionContext = resolveCodeExecutionContext({
statefulSessions: effectiveStatefulSessions,
environment: statefulCodeEnvironment,
environmentId: agent.code_environment_id,
environments:
req.config?.endpoints?.[EModelEndpoint.agents]?.statefulCodeSessions?.environments,
userId: requestFileOwnerId,
agentId: agent.id,
conversationId,

View file

@ -20,6 +20,7 @@ type VersionedAgent = Pick<
| 'skills_enabled'
| 'stateful_code_sessions'
| 'stateful_code_environment'
| 'code_environment_id'
| 'artifacts'
| 'recursion_limit'
| 'agent_ids'
@ -89,6 +90,7 @@ export function selectLazySubagentConfig(agent: VersionedAgent): Omit<VersionedA
skills_enabled,
stateful_code_sessions,
stateful_code_environment,
code_environment_id,
artifacts,
recursion_limit,
agent_ids,
@ -118,6 +120,7 @@ export function selectLazySubagentConfig(agent: VersionedAgent): Omit<VersionedA
skills_enabled,
stateful_code_sessions,
stateful_code_environment,
code_environment_id,
artifacts,
recursion_limit,
agent_ids,

View file

@ -1,4 +1,5 @@
import type { StatefulCodeEnvironment } from 'librechat-data-provider';
import type { CodeExecutionContext } from './execution';
import {
markSandboxReady,
maybePrewarmCodeSandbox,
@ -12,6 +13,7 @@ interface TestAgent {
id: string;
statefulCodeSessions?: boolean;
statefulCodeEnvironment?: StatefulCodeEnvironment;
codeExecutionContext?: CodeExecutionContext;
subagentAgentConfigs?: TestAgent[];
lazySubagentConfigs?: TestAgent[];
}
@ -93,6 +95,28 @@ describe('maybePrewarmCodeSandbox', () => {
await expect(shouldSignalSandboxStart('convo-1')).resolves.toBe(false);
});
it('does not prewarm an attached environment that leases a single worker', async () => {
const attachedAgent: TestAgent = {
id: 'agent_attached',
statefulCodeSessions: true,
codeExecutionContext: {
baseUrl: 'http://attached-code.test/v1',
codeSessionKey: 'execute_code:stateful:attached',
executionProfile: 'stateful',
runtimeSessionHint: 'v3:attached:conversation:abc',
statefulSessions: true,
environmentId: 'e2e-vm',
environmentType: 'attached',
},
};
maybePrewarmCodeSandbox({ req, conversationId: 'convo-1', agents: agents(attachedAgent) });
await flushAsync();
expect(fetchMock).not.toHaveBeenCalled();
await expect(shouldSignalSandboxStart('convo-1')).resolves.toBe(false);
});
it('walks subagent configs for the stateful gate', async () => {
const parent = { id: 'agent_parent', subagentAgentConfigs: [statefulAgent] };
maybePrewarmCodeSandbox({ req, conversationId: 'convo-1', agents: agents(parent) });
@ -196,6 +220,41 @@ describe('maybePrewarmCodeSandbox', () => {
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it('prewarms a replacement route even when its runtime session hint is unchanged', async () => {
const context = (executionRouteKey: string, baseUrl: string): TestAgent => ({
id: `agent-${executionRouteKey}`,
statefulCodeSessions: true,
codeExecutionContext: {
baseUrl,
codeSessionKey: `execute_code:${executionRouteKey}`,
executionProfile: 'stateful',
executionRouteKey,
runtimeSessionHint: 'v3:stable:user:scope',
statefulSessions: true,
environmentType: 'managed',
},
});
maybePrewarmCodeSandbox({
req,
conversationId: 'convo-1',
agents: agents(context(`stateful:${'a'.repeat(32)}`, 'https://old-code.example.com')),
});
await flushAsync();
maybePrewarmCodeSandbox({
req,
conversationId: 'convo-2',
agents: agents(context(`stateful:${'b'.repeat(32)}`, 'https://new-code.example.com')),
});
await flushAsync();
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(fetchMock.mock.calls.map(([url]) => url)).toEqual([
'https://old-code.example.com/exec',
'https://new-code.example.com/exec',
]);
});
it('does not refire while a prewarm is in flight', async () => {
fetchMock.mockImplementation(() => new Promise(() => undefined));
maybePrewarmCodeSandbox({ req, conversationId: 'convo-1', agents: agents(statefulAgent) });

View file

@ -15,6 +15,7 @@ type PrewarmAgent = {
id: string;
statefulCodeSessions?: boolean;
statefulCodeEnvironment?: StatefulCodeEnvironment;
codeExecutionContext?: CodeExecutionContext;
subagentAgentConfigs?: PrewarmAgent[];
lazySubagentConfigs?: PrewarmAgent[];
};
@ -73,14 +74,18 @@ function inflightKey(conversationId: string): string {
* any in-flight prewarm marker. Callers on hot paths should not await this;
* `void markSandboxReady(...)` is the expected usage.
*/
export async function markSandboxReady(conversationId: string): Promise<void> {
export async function markSandboxReady(
conversationId: string,
executionRouteKey?: string,
): Promise<void> {
if (!conversationId) {
return;
}
const cacheId = executionRouteKey ? `${executionRouteKey}:${conversationId}` : conversationId;
const cache = sandboxCache();
await Promise.all([
cache.set(readyKey(conversationId), true, coldAfterMs()),
cache.delete(inflightKey(conversationId)),
cache.set(readyKey(cacheId), true, coldAfterMs()),
cache.delete(inflightKey(cacheId)),
]);
}
@ -133,7 +138,10 @@ async function sendPrewarmRequest(
* Draining also releases the socket instead of leaving the body for
* undici to reap. */
await response.arrayBuffer();
await markSandboxReady(context.runtimeSessionHint ?? '');
await markSandboxReady(
context.runtimeSessionHint ?? '',
context.executionRouteKey ?? context.executionProfile,
);
logger.debug(`[prewarmCodeSandbox] Sandbox warm for ${context.runtimeSessionHint}`);
}
@ -153,14 +161,18 @@ function collectPrewarmContexts(
}
visited.add(agent.id);
if (agent.statefulCodeSessions === true) {
const context = resolveCodeExecutionContext({
statefulSessions: true,
environment: agent.statefulCodeEnvironment,
userId,
agentId: agent.id,
conversationId,
});
contexts.set(`${context.baseUrl}:${context.runtimeSessionHint}`, context);
const context =
agent.codeExecutionContext ??
resolveCodeExecutionContext({
statefulSessions: true,
environment: agent.statefulCodeEnvironment,
userId,
agentId: agent.id,
conversationId,
});
if (context.environmentType !== 'attached') {
contexts.set(`${context.baseUrl}:${context.runtimeSessionHint}`, context);
}
}
pending.push(...(agent.subagentAgentConfigs ?? []), ...(agent.lazySubagentConfigs ?? []));
}
@ -176,10 +188,11 @@ async function maybePrewarmContext(
if (!runtimeSessionHint) {
return true;
}
const runtimeCacheId = `${context.executionRouteKey ?? context.executionProfile}:${runtimeSessionHint}`;
const cache = sandboxCache();
const [ready, inflight] = await Promise.all([
cache.get(readyKey(runtimeSessionHint)),
cache.get(inflightKey(runtimeSessionHint)),
cache.get(readyKey(runtimeCacheId)),
cache.get(inflightKey(runtimeCacheId)),
]);
if (ready != null) {
return true;
@ -189,7 +202,7 @@ async function maybePrewarmContext(
return false;
}
await Promise.all([
cache.set(inflightKey(runtimeSessionHint), true, PREWARM_INFLIGHT_COOLDOWN_MS),
cache.set(inflightKey(runtimeCacheId), true, PREWARM_INFLIGHT_COOLDOWN_MS),
cache.set(inflightKey(conversationId), true, PREWARM_INFLIGHT_COOLDOWN_MS),
]);
await sendPrewarmRequest(req, context);

View file

@ -50,6 +50,7 @@ import type { ToolInputValidationError } from '~/agents/toolValidation';
import type { ResolvedToolApprovalHook } from '~/agents/hitl/hooks';
import type { TerminalSteerHook } from '~/agents/steering/runtime';
import type { ResolvedAlwaysApplySkill } from '~/agents/skills';
import type { CodeExecutionContext } from '~/agents/execution';
import type { MCPToolAlias } from '~/tools/classification';
import type { SubagentUsageEvent } from '~/agents/usage';
import type * as t from '~/types';
@ -423,6 +424,8 @@ type RunAgent = Omit<Agent, 'tools'> & {
statefulCodeEnvironment?: Agent['stateful_code_environment'];
/** Trusted partition for transient code session ids and file references. */
codeSessionKey?: string;
/** Trusted Code API route selected during initialization. */
codeExecutionContext?: CodeExecutionContext;
/** Optional per-agent summarization overrides */
summarization?: SummarizationConfig;
/** Response field to read model reasoning from for custom OpenAI-compatible endpoints. */
@ -466,6 +469,7 @@ type LazySubagentAgent = Pick<
| 'codeEnvAvailable'
| 'statefulCodeSessions'
| 'statefulCodeEnvironment'
| 'codeExecutionContext'
| 'codeSessionKey'
| 'includeReasoningHistory'
| 'mcpToolAliases'
@ -487,6 +491,7 @@ type SubagentTreeNode = Pick<
| 'codeEnvAvailable'
| 'statefulCodeSessions'
| 'statefulCodeEnvironment'
| 'codeExecutionContext'
| 'codeSessionKey'
| 'includeReasoningHistory'
| 'mcpToolAliases'

View file

@ -550,7 +550,7 @@ describe('primeInvokedSkillsForProfiles', () => {
mockExtract.mockReturnValue(new Set(['brand-guidelines']));
});
it('uploads and seeds historical skill files separately for default and stateful profiles', async () => {
it('uploads and seeds historical skill files separately for every Code API deployment', async () => {
const listSkillFiles = jest.fn().mockResolvedValue([
{
relativePath: 'references/style.md',
@ -563,15 +563,17 @@ describe('primeInvokedSkillsForProfiles', () => {
const getStrategyFunctions = jest.fn().mockReturnValue({
getDownloadStream: jest.fn().mockResolvedValue(Readable.from(Buffer.from('style'))),
});
const batchUploadCodeEnvFiles = jest.fn().mockImplementation(({ executionProfile }) => ({
storage_session_id: `${executionProfile}-session`,
files: [
{
fileId: `${executionProfile}-file`,
filename: 'skills/brand-guidelines/references/style.md',
},
],
}));
const batchUploadCodeEnvFiles = jest
.fn()
.mockImplementation(({ executionProfile, codeApiBaseUrl }) => ({
storage_session_id: `${executionProfile}-${new URL(codeApiBaseUrl).hostname}-session`,
files: [
{
fileId: `${executionProfile}-${new URL(codeApiBaseUrl).hostname}-file`,
filename: 'skills/brand-guidelines/references/style.md',
},
],
}));
const updateSkillFileCodeEnvIds = jest.fn().mockResolvedValue({
matchedCount: 1,
modifiedCount: 1,
@ -587,6 +589,7 @@ describe('primeInvokedSkillsForProfiles', () => {
updateSkillFileCodeEnvIds,
});
const statefulKey = 'execute_code:stateful:v2:user:abc';
const secondStatefulKey = 'execute_code:stateful:v2:user:def';
const deps: PrimeInvokedSkillsForProfilesDeps = {
...baseDeps,
executionProfiles: [
@ -604,32 +607,53 @@ describe('primeInvokedSkillsForProfiles', () => {
baseUrl: 'https://stateful.example.com/v1',
codeSessionKey: statefulKey,
executionProfile: 'stateful',
executionRouteKey: 'stateful:first',
runtimeSessionHint: 'v2:user:abc',
statefulSessions: true,
},
codeSessionKeys: [statefulKey],
},
{
codeExecutionContext: {
baseUrl: 'https://second-stateful.example.com/v1',
codeSessionKey: secondStatefulKey,
executionProfile: 'stateful',
executionRouteKey: 'stateful:second',
runtimeSessionHint: 'v2:user:def',
statefulSessions: true,
},
codeSessionKeys: [secondStatefulKey],
},
],
};
const result = await primeInvokedSkillsForProfiles(deps);
expect(batchUploadCodeEnvFiles).toHaveBeenCalledTimes(2);
expect(batchUploadCodeEnvFiles).toHaveBeenCalledTimes(3);
expect(
batchUploadCodeEnvFiles.mock.calls.map(([args]) => args.executionProfile).sort(),
).toEqual(['default', 'stateful']);
).toEqual(['default', 'stateful', 'stateful']);
expect(result.initialSessions?.get('execute_code')?.files?.map((file) => file.id)).toEqual([
'default-file',
'default-code.example.com-file',
]);
expect(result.initialSessions?.get(statefulKey)?.files?.map((file) => file.id)).toEqual([
'stateful-file',
'stateful-stateful.example.com-file',
]);
expect(updateSkillFileCodeEnvIds).toHaveBeenCalledTimes(2);
expect(result.initialSessions?.get(secondStatefulKey)?.files?.map((file) => file.id)).toEqual([
'stateful-second-stateful.example.com-file',
]);
expect(updateSkillFileCodeEnvIds).toHaveBeenCalledTimes(3);
expect(
updateSkillFileCodeEnvIds.mock.calls
.map(([updates]) => updates[0].codeEnvRef.executionProfile)
.sort(),
).toEqual(['default', 'stateful']);
).toEqual(['default', 'stateful', 'stateful']);
expect(
updateSkillFileCodeEnvIds.mock.calls
.map(([updates]) => updates[0].codeEnvRef.executionRouteKey)
.filter(Boolean)
.sort(),
).toEqual(['stateful:first', 'stateful:second']);
});
it('keeps a successful profile Skill body and identity after another profile lookup fails', async () => {

View file

@ -10,7 +10,6 @@ import {
} from 'librechat-data-provider';
import type { CodeEnvFile, ToolSessionMap, CodeSessionContext } from '@librechat/agents';
import type { Types } from 'mongoose';
import type { CodeExecutionContext } from './execution';
import type { ServerRequest } from '~/types';
import {
extractFileContent,
@ -22,6 +21,7 @@ import {
} from '~/protection';
import { seedCodeFilesIntoSessions, type CodeExecutionProfileRoute } from './codeFilesSession';
import { ContentFilterError, isContentFilterError } from '~/middleware/contentFilter';
import { getCodeExecutionRouteKey, type CodeExecutionContext } from './execution';
import { createConcurrencyLimiter, getSafeErrorMetadata } from '~/utils';
import { assertSkillFileContentAllowed } from '~/skills/protection';
import { createSkillContentDigest } from './compatibility';
@ -89,7 +89,10 @@ export interface PrimeSkillFilesParams {
},
) => Promise<string | null>;
/** Trusted Code API route selected for the executing agent. */
codeExecutionContext?: Pick<CodeExecutionContext, 'baseUrl' | 'executionProfile'>;
codeExecutionContext?: Pick<
CodeExecutionContext,
'baseUrl' | 'executionProfile' | 'executionRouteKey'
>;
/** 23-hour freshness check */
checkIfActive?: (dateString: string) => boolean;
/** Persists `codeEnvRef` on skill files after upload. Implementations
@ -344,7 +347,10 @@ export async function primeSkillFiles(
* resource-scoped (`<tenant>:skill:<id>:v:<version>`), so sharing the
* result across requests is sound. Per-process best-effort; the awaited
* codeEnvRef persist covers cross-turn and cross-node dedupe. */
const flightKey = `${params.codeExecutionContext?.executionProfile ?? 'default'}:${params.skill._id}:v:${params.skill.version}`;
const executionRouteKey = params.codeExecutionContext
? getCodeExecutionRouteKey(params.codeExecutionContext)
: 'default';
const flightKey = `${executionRouteKey}:${params.skill._id}:v:${params.skill.version}`;
const inflight = inflightPrimes.get(flightKey);
if (inflight) {
return inflight;
@ -371,6 +377,9 @@ async function executePrimeSkillFiles(
codeExecutionContext,
} = params;
const executionProfile = codeExecutionContext?.executionProfile ?? 'default';
const executionRouteKey = codeExecutionContext
? getCodeExecutionRouteKey(codeExecutionContext)
: executionProfile;
const inspectStoredMetadata = shouldInspectStoredSkillFileMetadata(req);
const inspectBundledFileContent = shouldInspectStoredSkillFileContent(req);
const inspectedBuffers = new Map<SkillFileRecord, Buffer>();
@ -422,12 +431,12 @@ async function executePrimeSkillFiles(
* upload on the next prime. */
if (getSessionInfo && checkIfActive && skillFiles.length > 0) {
const allHaveRefs = skillFiles.every(
(sf) => getCodeEnvRefForProfile(sf, executionProfile) !== undefined,
(sf) => getCodeEnvRefForProfile(sf, executionRouteKey) !== undefined,
);
if (allHaveRefs) {
const refsBySession = new Map<string, CodeEnvRef>();
for (const sf of skillFiles) {
const ref = getCodeEnvRefForProfile(sf, executionProfile);
const ref = getCodeEnvRefForProfile(sf, executionRouteKey);
if (ref && !refsBySession.has(ref.storage_session_id)) {
refsBySession.set(ref.storage_session_id, ref);
}
@ -445,7 +454,7 @@ async function executePrimeSkillFiles(
if (allActive) {
const files: PrimeSkillFilesResult['files'] = [];
for (const sf of skillFiles) {
const ref = getCodeEnvRefForProfile(sf, executionProfile);
const ref = getCodeEnvRefForProfile(sf, executionRouteKey);
if (!ref) continue;
/* Cache-hit refs already carry resource identity (kind / id /
* version) — pull them through so the artifact emitted by
@ -574,6 +583,7 @@ async function executePrimeSkillFiles(
file_id: f.fileId,
version: skill.version,
executionProfile,
...(executionRouteKey !== executionProfile ? { executionRouteKey } : {}),
};
return {
skillId: skill._id,
@ -737,12 +747,15 @@ export async function primeInvokedSkills(
// ALL distinct sessions for freshness. If all are active, return cached
// references with zero re-uploads. If any expired, re-upload everything.
const executionProfile = deps.codeExecutionContext?.executionProfile ?? 'default';
const executionRouteKey = deps.codeExecutionContext
? getCodeExecutionRouteKey(deps.codeExecutionContext)
: executionProfile;
if (!inspectStoredSkillFileContent && deps.getSessionInfo && deps.checkIfActive) {
const allResolved = fileListResults.flatMap((r) =>
r.files.map((f) => ({
skillName: r.skill.name,
file: f,
ref: getCodeEnvRefForProfile(f, executionProfile),
ref: getCodeEnvRefForProfile(f, executionRouteKey),
})),
);
const resolvedWithRef = allResolved.filter((x) => x.ref !== undefined);

View file

@ -400,6 +400,8 @@ export const agentSubagentsSchema: z.ZodOptional<z.ZodType<AgentSubagentsConfig>
.optional();
/** Base agent schema with all common fields */
const agentCodeEnvironmentIdSchema = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/);
export const agentBaseSchema: z.ZodObject<
{
name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@ -486,6 +488,7 @@ export const agentBaseSchema: z.ZodObject<
hide_sequential_outputs: z.ZodOptional<z.ZodBoolean>;
stateful_code_sessions: z.ZodOptional<z.ZodBoolean>;
stateful_code_environment: z.ZodOptional<z.ZodEnum<['user', 'agent-user', 'conversation']>>;
code_environment_id: z.ZodOptional<z.ZodString>;
artifacts: z.ZodOptional<z.ZodString>;
recursion_limit: z.ZodOptional<z.ZodNumber>;
conversation_starters: z.ZodOptional<z.ZodArray<z.ZodString, 'many'>>;
@ -558,6 +561,7 @@ export const agentBaseSchema: z.ZodObject<
hide_sequential_outputs: z.boolean().optional(),
stateful_code_sessions: z.boolean().optional(),
stateful_code_environment: z.enum(['user', 'agent-user', 'conversation']).optional(),
code_environment_id: agentCodeEnvironmentIdSchema.optional(),
artifacts: z.string().optional(),
recursion_limit: z.number().optional(),
conversation_starters: z.array(z.string()).optional(),
@ -653,6 +657,7 @@ export const agentCreateSchema: z.ZodObject<
hide_sequential_outputs: z.ZodOptional<z.ZodBoolean>;
stateful_code_sessions: z.ZodOptional<z.ZodBoolean>;
stateful_code_environment: z.ZodOptional<z.ZodEnum<['user', 'agent-user', 'conversation']>>;
code_environment_id: z.ZodOptional<z.ZodString>;
artifacts: z.ZodOptional<z.ZodString>;
recursion_limit: z.ZodOptional<z.ZodNumber>;
conversation_starters: z.ZodOptional<z.ZodArray<z.ZodString, 'many'>>;
@ -784,6 +789,7 @@ export const agentUpdateSchema: z.ZodObject<
hide_sequential_outputs: z.ZodOptional<z.ZodBoolean>;
stateful_code_sessions: z.ZodOptional<z.ZodBoolean>;
stateful_code_environment: z.ZodOptional<z.ZodEnum<['user', 'agent-user', 'conversation']>>;
code_environment_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
artifacts: z.ZodOptional<z.ZodString>;
recursion_limit: z.ZodOptional<z.ZodNumber>;
conversation_starters: z.ZodOptional<z.ZodArray<z.ZodString, 'many'>>;
@ -867,6 +873,7 @@ export const agentUpdateSchema: z.ZodObject<
'strip'
> = agentBaseSchema.extend({
avatar: z.union([agentAvatarSchema, z.null()]).optional(),
code_environment_id: agentCodeEnvironmentIdSchema.nullable().optional(),
provider: z.string().optional(),
model: z.string().nullable().optional(),
});

View file

@ -186,7 +186,18 @@ describe('createEndpointsConfigService', () => {
appConfig({
endpoints: {
[EModelEndpoint.agents]: {
statefulCodeSessions: { allowedEnvironments: ['user', 'agent-user'] },
statefulCodeSessions: {
allowedEnvironments: ['user', 'agent-user'],
environments: [
{
id: 'attached-vm',
name: 'Attached VM',
type: 'attached',
baseURL: 'https://internal-code.example.com/v1',
default: true,
},
],
},
},
},
}),
@ -198,6 +209,14 @@ describe('createEndpointsConfigService', () => {
expect(result?.[EModelEndpoint.agents]?.statefulCodeSessions).toEqual({
allowedEnvironments: ['user', 'agent-user'],
environments: [
{
id: 'attached-vm',
name: 'Attached VM',
type: 'attached',
default: true,
},
],
});
});

View file

@ -72,12 +72,25 @@ export function createEndpointsConfigService(deps: EndpointsConfigDeps): {
if (mergedConfig[EModelEndpoint.agents] && appConfig?.endpoints?.[EModelEndpoint.agents]) {
const { disableBuilder, capabilities, allowedProviders, statefulCodeSessions, maxSubagents } =
appConfig.endpoints[EModelEndpoint.agents];
const clientStatefulCodeSessions = statefulCodeSessions
? {
allowedEnvironments: statefulCodeSessions.allowedEnvironments,
environments: statefulCodeSessions.environments?.map(
({ id, name, type, default: isDefault }) => ({
id,
name,
type,
default: isDefault,
}),
),
}
: undefined;
mergedConfig[EModelEndpoint.agents] = {
...mergedConfig[EModelEndpoint.agents],
allowedProviders,
disableBuilder,
capabilities,
statefulCodeSessions,
statefulCodeSessions: clientStatefulCodeSessions,
maxSubagents,
};
}

View file

@ -449,6 +449,99 @@ describe('agentsEndpointSchema', () => {
expect(result.success).toBe(true);
});
it('accepts uniquely named execution environments with exactly one default', () => {
const result = agentsEndpointSchema.safeParse({
statefulCodeSessions: {
allowedEnvironments: ['conversation'],
environments: [
{
id: 'managed',
name: 'Managed',
type: 'managed',
baseURL: 'https://code.example.com/v1',
default: true,
},
{
id: 'attached-vm',
name: 'Attached VM',
type: 'attached',
baseURL: 'https://bridge.example.com/v1',
},
],
},
});
expect(result.success).toBe(true);
});
it.each([
'ftp://code.example.com/v1',
'https://code.example.com/v1?',
'https://code.example.com/v1?token=secret',
'https://code.example.com/v1#',
'https://code.example.com/v1#fragment',
])('rejects a non-base execution environment URL: %s', (baseURL) => {
const result = agentsEndpointSchema.safeParse({
statefulCodeSessions: {
allowedEnvironments: ['conversation'],
environments: [
{
id: 'attached-vm',
name: 'Attached VM',
type: 'attached',
baseURL,
default: true,
},
],
},
});
expect(result.success).toBe(false);
});
it('normalizes surrounding whitespace in an execution environment URL', () => {
const result = agentsEndpointSchema.safeParse({
statefulCodeSessions: {
allowedEnvironments: ['conversation'],
environments: [
{
id: 'attached-vm',
name: 'Attached VM',
type: 'attached',
baseURL: ' https://bridge.example.com/v1/ ',
default: true,
},
],
},
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.statefulCodeSessions?.environments?.[0]?.baseURL).toBe(
'https://bridge.example.com/v1/',
);
}
});
it('rejects ambiguous execution environment routing', () => {
const environment = {
id: 'attached-vm',
name: 'Attached VM',
type: 'attached',
baseURL: 'https://bridge.example.com/v1',
} as const;
const parse = (environments: unknown[]) =>
agentsEndpointSchema.safeParse({
statefulCodeSessions: {
allowedEnvironments: ['conversation'],
environments,
},
});
expect(parse([environment]).success).toBe(false);
expect(parse([{ ...environment, default: true }, { ...environment }]).success).toBe(false);
});
it('defaults maxSubagents to MAX_SUBAGENTS and validates its bounds', () => {
const omitted = agentsEndpointSchema.safeParse({});
expect(omitted.success).toBe(true);

View file

@ -88,4 +88,28 @@ describe('CodeEnvRef', () => {
expect(getCodeEnvRefForProfile({ codeEnvRef: legacy }, 'default')).toBe(legacy);
expect(getCodeEnvRefForProfile({ codeEnvRef: legacy }, 'stateful')).toBeUndefined();
});
it('retains independent pointers for configured environments sharing one profile', () => {
const first: CodeEnvRef = {
kind: 'skill',
id: 'skill-1',
version: 1,
storage_session_id: 'first-session',
file_id: 'first-file',
executionProfile: 'stateful',
executionRouteKey: 'stateful:first',
};
const second: CodeEnvRef = {
...first,
storage_session_id: 'second-session',
file_id: 'second-file',
executionRouteKey: 'stateful:second',
};
const refs = mergeCodeEnvRef(mergeCodeEnvRef(undefined, first), second);
expect(getCodeEnvRefForProfile(refs, 'stateful:first')).toBe(first);
expect(getCodeEnvRefForProfile(refs, 'stateful:second')).toBe(second);
expect(Object.keys(refs.codeEnvRefs)).toEqual(['stateful:first', 'stateful:second']);
});
});

View file

@ -54,6 +54,10 @@ interface CodeEnvRefBase {
/** Code API deployment that owns this storage pointer. Legacy refs omit
* the field and are treated as `default`; new writes always persist it. */
executionProfile?: CodeExecutionProfile;
/** Deployment-specific namespace. Configured stateful environments share
* the same wire profile, so this key prevents their file IDs from being
* reused against a different Code API server. */
executionRouteKey?: string;
}
export type CodeExecutionProfile = 'default' | 'stateful';
@ -64,7 +68,7 @@ export type CodeEnvRef =
| (CodeEnvRefBase & { kind: 'user' });
/** Deployment-local pointers for a resource that may be used by both Code API profiles. */
export type CodeEnvRefMap = Partial<Record<CodeExecutionProfile, CodeEnvRef>>;
export type CodeEnvRefMap = Partial<Record<string, CodeEnvRef>>;
export interface CodeEnvReferenceSet {
/** Compatibility pointer for readers that have not adopted profile-aware refs yet. */
@ -75,14 +79,17 @@ export interface CodeEnvReferenceSet {
export function getCodeEnvRefForProfile(
refs: CodeEnvReferenceSet | null | undefined,
profile: CodeExecutionProfile,
routeKey: string,
): CodeEnvRef | undefined {
const profileRef = refs?.codeEnvRefs?.[profile];
const profileRef = refs?.codeEnvRefs?.[routeKey];
if (profileRef) {
return profileRef;
}
const legacyRef = refs?.codeEnvRef;
if (legacyRef && (legacyRef.executionProfile ?? 'default') === profile) {
if (
legacyRef &&
(legacyRef.executionRouteKey ?? legacyRef.executionProfile ?? 'default') === routeKey
) {
return legacyRef;
}
return undefined;
@ -96,12 +103,13 @@ export function mergeCodeEnvRef(
const codeEnvRefs: CodeEnvRefMap = { ...refs?.codeEnvRefs };
const legacyRef = refs?.codeEnvRef;
if (legacyRef) {
codeEnvRefs[legacyRef.executionProfile ?? 'default'] ??= legacyRef;
codeEnvRefs[legacyRef.executionRouteKey ?? legacyRef.executionProfile ?? 'default'] ??=
legacyRef;
}
const profile = ref.executionProfile ?? 'default';
codeEnvRefs[profile] = ref;
const routeKey = ref.executionRouteKey ?? ref.executionProfile ?? 'default';
codeEnvRefs[routeKey] = ref;
return {
codeEnvRef: codeEnvRefs.default ?? codeEnvRefs.stateful!,
codeEnvRef: codeEnvRefs.default ?? codeEnvRefs.stateful ?? ref,
codeEnvRefs,
};
}
@ -109,14 +117,13 @@ export function mergeCodeEnvRef(
/** Enumerates every deployment-local pointer, including legacy single-pointer records. */
export function getCodeEnvRefs(
refs: CodeEnvReferenceSet | null | undefined,
): Array<[CodeExecutionProfile, CodeEnvRef]> {
): Array<[string, CodeEnvRef]> {
const merged: CodeEnvRefMap = { ...refs?.codeEnvRefs };
const legacyRef = refs?.codeEnvRef;
if (legacyRef) {
merged[legacyRef.executionProfile ?? 'default'] ??= legacyRef;
merged[legacyRef.executionRouteKey ?? legacyRef.executionProfile ?? 'default'] ??= legacyRef;
}
return (['default', 'stateful'] as const).flatMap((profile) => {
const ref = merged[profile];
return ref ? [[profile, ref] as [CodeExecutionProfile, CodeEnvRef]] : [];
});
return Object.entries(merged).flatMap(([routeKey, ref]) =>
ref ? [[routeKey, ref] as [string, CodeEnvRef]] : [],
);
}

View file

@ -1001,6 +1001,28 @@ export const checkpointerSchema = z
export type TCheckpointerConfig = z.infer<typeof checkpointerSchema>;
const codeEnvironmentBaseURLSchema = z
.string()
.trim()
.url()
.refine(
(value) => {
try {
const url = new URL(value);
return (
(url.protocol === 'http:' || url.protocol === 'https:') &&
!value.includes('?') &&
!value.includes('#') &&
url.search.length === 0 &&
url.hash.length === 0
);
} catch {
return false;
}
},
{ message: 'Code environment baseURL must be an HTTP(S) base URL without query or fragment' },
);
export const agentsEndpointSchema = baseEndpointSchema
.omit({ baseURL: true })
.merge(
@ -1041,6 +1063,42 @@ export const agentsEndpointSchema = baseEndpointSchema
statefulCodeSessions: z
.object({
allowedEnvironments: z.array(z.enum(STATEFUL_CODE_ENVIRONMENTS)).min(1),
/** Operator-managed execution environments. Attached entries route to a
* Code API deployment backed by an outbound librechat-code worker. */
environments: z
.array(
z.object({
id: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/),
name: z.string().min(1).max(100),
type: z.enum(['managed', 'attached']),
baseURL: codeEnvironmentBaseURLSchema,
default: z.boolean().optional(),
}),
)
.optional(),
})
.superRefine((value, context) => {
if (!value?.environments) return;
const ids = new Set<string>();
let defaults = 0;
for (const environment of value.environments) {
if (ids.has(environment.id)) {
context.addIssue({
code: z.ZodIssueCode.custom,
message: `Duplicate code environment id: ${environment.id}`,
path: ['environments'],
});
}
ids.add(environment.id);
if (environment.default === true) defaults += 1;
}
if (value.environments.length > 0 && defaults !== 1) {
context.addIssue({
code: z.ZodIssueCode.custom,
message: 'Exactly one stateful code environment must be the default',
path: ['environments'],
});
}
})
.optional(),
/** Optional trusted origin for in-process agent event delivery. */

View file

@ -349,6 +349,7 @@ export const defaultAgentFormValues = {
[Tools.web_search]: false,
[Tools.memory]: false,
stateful_code_environment: 'user' as const,
code_environment_id: undefined as string | null | undefined,
category: 'general',
support_contact: {
name: '',

View file

@ -526,6 +526,14 @@ export type TSearchResults = {
filter: object;
};
export type TPublicCodeEnvironment = {
id: string;
name: string;
type: 'managed' | 'attached';
default?: boolean;
pairingAvailable?: boolean;
};
export type TConfig = {
order: number;
type?: EModelEndpoint;
@ -551,6 +559,7 @@ export type TConfig = {
capabilities?: string[];
statefulCodeSessions?: {
allowedEnvironments: StatefulCodeEnvironment[];
environments?: TPublicCodeEnvironment[];
};
/** Effective subagents-per-agent cap served from `endpoints.agents.maxSubagents`. */
maxSubagents?: number;

View file

@ -337,6 +337,8 @@ export type Agent = {
stateful_code_sessions?: boolean;
/** Stateful workspace sharing scope. Defaults to one workspace per user. */
stateful_code_environment?: StatefulCodeEnvironment;
/** Operator-configured managed or attached stateful execution environment. */
code_environment_id?: string | null;
artifacts?: ArtifactModes;
recursion_limit?: number;
isPublic?: boolean;
@ -392,6 +394,7 @@ export type AgentCreateParams = {
| 'hide_sequential_outputs'
| 'stateful_code_sessions'
| 'stateful_code_environment'
| 'code_environment_id'
| 'artifacts'
| 'recursion_limit'
| 'category'
@ -422,6 +425,7 @@ export type AgentUpdateParams = {
| 'hide_sequential_outputs'
| 'stateful_code_sessions'
| 'stateful_code_environment'
| 'code_environment_id'
| 'artifacts'
| 'recursion_limit'
| 'category'

View file

@ -2034,6 +2034,23 @@ describe('Agent Methods', () => {
expect(thirdUpdate!.versions).toHaveLength(4);
});
test('should unset a field and omit it from the recorded version', async () => {
const agentId = `agent_${uuidv4()}`;
await createAgent({
id: agentId,
provider: 'test',
model: 'test-model',
author: new mongoose.Types.ObjectId(),
code_environment_id: 'attached-vm',
});
const updated = await updateAgent({ id: agentId }, { $unset: { code_environment_id: 1 } });
expect(updated!.code_environment_id).toBeUndefined();
expect(updated!.versions).toHaveLength(2);
expect(updated!.versions![1].code_environment_id).toBeUndefined();
});
test('should handle parameter objects correctly', async () => {
const agentId = `agent_${uuidv4()}`;
const authorId = new mongoose.Types.ObjectId();
@ -2529,6 +2546,24 @@ describe('Agent Methods', () => {
expect(revertedAgent.description).toBe('Original description');
});
test('should clear an explicit code environment when the restored version used the default', async () => {
const agentId = `agent_${uuidv4()}`;
const authorId = new mongoose.Types.ObjectId();
await createAgent({
id: agentId,
name: 'Default Environment Agent',
provider: 'test',
model: 'test-model',
author: authorId,
});
await updateAgent({ id: agentId }, { code_environment_id: 'replacement-environment' });
const revertedAgent = await revertAgentVersion({ id: agentId }, 0);
expect(revertedAgent.code_environment_id).toBeUndefined();
});
test('should prune deleted skill ids when reverting to an older version', async () => {
const agentId = `agent_${uuidv4()}`;
const authorId = new mongoose.Types.ObjectId();

View file

@ -203,6 +203,20 @@ function resolveDocumentPath(source: Record<string, unknown>, path: string): unk
return current;
}
/** Removes a dotted operator path from an in-memory version projection. */
function deleteDocumentPath(source: Record<string, unknown>, path: string): void {
const segments = path.split('.');
const leaf = segments.pop();
if (leaf == null) return;
let current: Record<string, unknown> = source;
for (const segment of segments) {
const next = current[segment];
if (typeof next !== 'object' || next === null || next instanceof Map) return;
current = next as Record<string, unknown>;
}
delete current[leaf];
}
/** The values an `$addToSet` specification would add, flattening the `$each` form. */
function addToSetCandidates(spec: unknown): unknown[] {
if (
@ -228,11 +242,18 @@ function operatorsMutateDocument(
$push: unknown,
$pull: unknown,
$addToSet: unknown,
$unset: unknown,
): boolean {
if (hasOperatorKeys($push) || hasOperatorKeys($pull)) {
return true;
}
if (hasOperatorKeys($unset)) {
for (const path of Object.keys($unset as Record<string, unknown>)) {
if (resolveDocumentPath(currentObject, path) !== undefined) return true;
}
}
if (!hasOperatorKeys($addToSet)) {
return false;
}
@ -280,13 +301,24 @@ function isDuplicateVersion(
'actionsHash',
];
const { $push: _$push, $pull: _$pull, $addToSet: _$addToSet, ...directUpdates } = updateData;
const {
$push: _$push,
$pull: _$pull,
$addToSet: _$addToSet,
$unset,
...directUpdates
} = updateData;
if (Object.keys(directUpdates).length === 0 && !actionsHash) {
if (Object.keys(directUpdates).length === 0 && !hasOperatorKeys($unset) && !actionsHash) {
return null;
}
const wouldBeVersion = { ...currentData, ...directUpdates } as Record<string, unknown>;
if (hasOperatorKeys($unset)) {
for (const path of Object.keys($unset as Record<string, unknown>)) {
deleteDocumentPath(wouldBeVersion, path);
}
}
const lastVersion = versions[versions.length - 1] as Record<string, unknown>;
if (actionsHash && lastVersion.actionsHash !== actionsHash) {
@ -656,7 +688,7 @@ export function createAgentMethods(
if (currentAgent) {
const currentObject = currentAgent.toObject() as unknown as Record<string, unknown>;
const { __v, _id, id: __id, versions, author: _author, ...versionData } = currentObject;
const { $push, $pull, $addToSet, ...directUpdates } = updateData;
const { $push, $pull, $addToSet, $unset, ...directUpdates } = updateData;
/** Self-heal: drop allowlist ids whose skill no longer exists in the
* database or the external registry.
@ -725,7 +757,12 @@ export function createAgentMethods(
const shouldCreateVersion =
!skipVersioning &&
(forceVersion || Object.keys(directUpdates).length > 0 || $push || $pull || $addToSet);
(forceVersion ||
Object.keys(directUpdates).length > 0 ||
$push ||
$pull ||
$addToSet ||
$unset);
if (shouldCreateVersion) {
const duplicateVersion = isDuplicateVersion(
@ -746,6 +783,7 @@ export function createAgentMethods(
$push,
$pull,
$addToSet,
$unset,
);
if (duplicateVersion && !forceVersion && !mutatesOutsideSnapshot) {
suppressedVersionEntry = true;
@ -759,6 +797,7 @@ export function createAgentMethods(
delete updateData.$addToSet;
delete updateData.$push;
delete updateData.$pull;
delete updateData.$unset;
}
}
@ -767,6 +806,11 @@ export function createAgentMethods(
...directUpdates,
updatedAt: new Date(),
};
if (hasOperatorKeys($unset)) {
for (const path of Object.keys($unset as Record<string, unknown>)) {
deleteDocumentPath(versionEntry, path);
}
}
if (actionsHash) {
versionEntry.actionsHash = actionsHash;
@ -1200,7 +1244,14 @@ export function createAgentMethods(
}
}
const revertedAgent = await Agent.findOneAndUpdate(searchParameter, revertToVersion, {
const restoresDeploymentDefault = !Object.prototype.hasOwnProperty.call(
revertToVersion,
'code_environment_id',
);
const revertUpdate = restoresDeploymentDefault
? { $set: revertToVersion, $unset: { code_environment_id: 1 } }
: { $set: revertToVersion };
const revertedAgent = await Agent.findOneAndUpdate(searchParameter, revertUpdate, {
new: true,
}).lean<IAgent>();
if (!revertedAgent) {

View file

@ -2082,6 +2082,45 @@ describe('SkillFile methods', () => {
expect(file.codeEnvRefs?.stateful?.file_id).toBe('stateful-file');
});
it('persists deployment-specific route keys and their reference metadata', async () => {
const { skill } = await methods.createSkill(makeSkillInput());
await methods.upsertSkillFile({
skillId: skill._id,
relativePath: 'scripts/a.sh',
file_id: 'f1',
filename: 'a.sh',
filepath: '/a',
source: 'local',
mimeType: 'text/plain',
bytes: 1,
author: owner._id,
});
const executionRouteKey = 'stateful:0123456789abcdef0123456789abcdef';
await methods.updateSkillFileCodeEnvIds([
{
skillId: skill._id,
relativePath: 'scripts/a.sh',
codeEnvRef: {
kind: 'skill',
id: skill._id.toString(),
version: 1,
storage_session_id: 'route-session',
file_id: 'route-file',
executionProfile: 'stateful',
executionRouteKey,
},
},
]);
const [file] = await methods.listSkillFiles(skill._id);
expect(file.codeEnvRefs?.[executionRouteKey]).toMatchObject({
file_id: 'route-file',
executionProfile: 'stateful',
executionRouteKey,
});
});
it('reports modifiedCount=0 when no SkillFile rows match the (skillId, relativePath) filter', async () => {
const { skill } = await methods.createSkill(makeSkillInput());
const result = await methods.updateSkillFileCodeEnvIds([

View file

@ -1888,14 +1888,14 @@ export function createSkillMethods(
if (updates.length === 0) return { matchedCount: 0, modifiedCount: 0 };
const SkillFile = mongoose.models.SkillFile as Model<ISkillFileDocument>;
const ops = updates.map((u) => {
const profile = u.codeEnvRef.executionProfile ?? 'default';
const routeKey = u.codeEnvRef.executionRouteKey ?? u.codeEnvRef.executionProfile ?? 'default';
return {
updateOne: {
filter: { skillId: u.skillId, relativePath: u.relativePath },
update: {
$set: {
codeEnvRef: u.codeEnvRef,
[`codeEnvRefs.${profile}`]: u.codeEnvRef,
[`codeEnvRefs.${routeKey}`]: u.codeEnvRef,
},
},
},

View file

@ -81,6 +81,9 @@ const agentSchema: Schema<IAgent> = new Schema<IAgent>(
type: String,
enum: ['user', 'agent-user', 'conversation'],
},
code_environment_id: {
type: String,
},
/** @deprecated Use edges instead */
agent_ids: {
type: [String],

View file

@ -15,14 +15,15 @@ export const codeEnvRefSchema: Schema = new Schema(
type: String,
enum: ['default', 'stateful'],
},
executionRouteKey: { type: String },
},
{ _id: false },
);
export const codeEnvRefMapSchema: Schema = new Schema(
{
default: { type: codeEnvRefSchema, default: undefined },
stateful: { type: codeEnvRefSchema, default: undefined },
},
{ _id: false },
);
/** Route keys are `default`, `stateful`, or `stateful:<deployment hash>`.
* Mixed preserves dot-addressed dynamic keys as a plain object; a Mongoose
* Map would require every existing hydrated-document reader to switch from
* bracket access to `.get()`. Values are produced through the typed
* CodeEnvRef write paths and the singular compatibility field remains fully
* schema-validated. */
export const codeEnvRefMapSchema: typeof Schema.Types.Mixed = Schema.Types.Mixed;

View file

@ -38,6 +38,7 @@ export interface IAgent extends Omit<Document, 'model'> {
end_after_tools?: boolean;
stateful_code_sessions?: boolean;
stateful_code_environment?: 'user' | 'agent-user' | 'conversation';
code_environment_id?: string;
/** @deprecated Use edges instead */
agent_ids?: string[];
edges?: GraphEdge[];