Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0d9630c
feat(code): add helpers for the stateful code default profile setting
TomasPalsson Oct 16, 2026
b45205c
feat(code): run agents and subagents stateful under the stateful defa…
TomasPalsson Oct 16, 2026
7468963
feat(code): resolve stateful sessions for legacy tool loading under t…
TomasPalsson Oct 16, 2026
a4d4a25
fix(code): keep code on agents when the stateful default cannot apply
TomasPalsson Oct 16, 2026
4eec50b
feat(code): default code-output downloads to the stateful profile whe…
TomasPalsson Oct 16, 2026
2a7b000
fix(code): opt stale-environment agents out of no-workspace chats und…
TomasPalsson Oct 16, 2026
b50c04d
docs: document LIBRECHAT_CODE_DEFAULT_PROFILE in .env.example
TomasPalsson Oct 16, 2026
4793351
fix(code): apply the stateful default only to agents not on an attach…
TomasPalsson Oct 16, 2026
1f03eb1
test(files): cover the stateful profile label on code file provisioning
TomasPalsson Oct 16, 2026
4d8ae93
🧪 test: Cover Stateful Default Setting in Lazy Subagent Metadata
TomasPalsson Oct 16, 2026
bc88e54
🐛 fix: Keep Code Downloads on Default Route When Stateful Capability …
TomasPalsson Oct 16, 2026
3b9b988
🏷️ test: Type the Provisioning Fixture Source as FileSources.local
TomasPalsson Oct 16, 2026
569bcf3
📝 docs: Note the Attached-Machine and Capability Limits of LIBRECHAT_…
TomasPalsson Oct 16, 2026
9a0ee37
fix(code): keep base behavior when the stateful default scope is not …
TomasPalsson Oct 16, 2026
58b075f
fix(code): pin default-profile download links when the deployment def…
TomasPalsson Oct 16, 2026
1635e97
📝 docs: Note the allowedEnvironments Scope Limit of LIBRECHAT_CODE_DE…
TomasPalsson Oct 16, 2026
0ac747e
Merge branch 'dev' into feat/stateful-code-default-profile
TomasPalsson Oct 16, 2026
5699188
🧪 test: Assert the Exact Policy Error for an Explicit Stateful Agent …
TomasPalsson Oct 16, 2026
514603c
🧪 test: Cover Stateful Code Default Profile at Agent Initialization
TomasPalsson Oct 16, 2026
aee92b5
Merge branch 'feat/stateful-code-default-profile' of github.com:Tomas…
TomasPalsson Oct 16, 2026
8be745e
🧭 feat: Show Stateful Code Options When the Deployment Runs Code Stat…
TomasPalsson Oct 16, 2026
4359288
🔀 chore: Merge dev into feat/stateful-code-default-profile
TomasPalsson Oct 19, 2026
6ebc237
🧪 test: Mock Config Service in File List and Preview Route Specs
TomasPalsson Oct 19, 2026
8e3ab8d
🔀 chore: Merge dev into feat/stateful-code-default-profile
TomasPalsson Oct 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,16 @@ TTS_API_KEY=
# When configured, stateless agents continue using LIBRECHAT_CODE_BASEURL while stateful
# agents fail closed onto this endpoint. The endpoint must advertise the `stateful` profile.
# LIBRECHAT_CODE_BASEURL_STATEFUL=
# Code execution profile for chat users: `default` or `stateful` (default: `default`).
# `stateful` runs every agent with code execution, including normal chats with the Code toggle on,
# on the stateful profile and overrides an agent's saved "stateful sessions: off". Requires the
# `stateful_code_sessions` agents capability and a Code API that advertises the `stateful` profile;
# without the capability, code and its downloads stay on the default profile. Agents whose code
# machine is an attached environment, or names one that is not configured, keep their saved setting,
# as do agents whose stateful scope is not in statefulCodeSessions.allowedEnvironments (normal chats
# use the `user` scope).
# Unrecognized values are ignored with a warning.
# LIBRECHAT_CODE_DEFAULT_PROFILE=default
# Prewarm selected stateful sandboxes in parallel with model generation (default: true).
# CODE_SANDBOX_PREWARM=true
# Time in milliseconds before LibreChat treats a tracked sandbox as cold (default: 2100000 / 35 minutes).
Expand Down
3 changes: 2 additions & 1 deletion api/app/clients/tools/util/handleTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const {
withRequestCodeInputs,
resolveAgentCodeExecution,
resolveMCPClientCapabilityProfile,
isStatefulCodeDefaultProfile,
} = require('@librechat/api');
const {
AuthType,
Expand Down Expand Up @@ -321,7 +322,7 @@ const loadTools = async ({
agent,
codeExecutionAvailable: true,
statefulSessionsAvailable:
agent?.stateful_code_sessions === true &&
(agent?.stateful_code_sessions === true || isStatefulCodeDefaultProfile()) &&
(await checkCapability(options.req, AgentCapabilities.stateful_code_sessions)),
userId: user,
conversationId: options.req?.body?.conversationId,
Expand Down
63 changes: 63 additions & 0 deletions api/app/clients/tools/util/handleTools.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const mockGetMCPServerTools = jest.fn();
const mockCreateMCPTool = jest.fn();
const mockCreateMCPTools = jest.fn();
const mockGetServerConfig = jest.fn();
const mockCheckCapability = jest.fn(async () => true);
const mockGetAccessibleMcpServerNames = jest.fn(async () => []);
const mockPrimeCodeFiles = jest.fn(async () => ({ files: [], toolContext: undefined }));

Expand Down Expand Up @@ -57,6 +58,7 @@ jest.mock('~/server/services/Config', () => ({
},
}),
getMCPServerTools: (...args) => mockGetMCPServerTools(...args),
checkCapability: (...args) => mockCheckCapability(...args),
}));

jest.mock('~/server/services/MCP', () => ({
Expand Down Expand Up @@ -375,6 +377,67 @@ describe('Tool Handlers', () => {
);
});

describe('stateful default profile', () => {
const loadCodeTool = async (agent) => {
const toolMap = await loadTools({
user: fakeUser._id.toString(),
tools: [Tools.execute_code],
returnMap: true,
agent,
options: { req: { body: {} } },
});
await toolMap[Tools.execute_code]();
return mockPrimeCodeFiles.mock.calls.at(-1)[0].executionProfile;
};

beforeEach(() => {
process.env.LIBRECHAT_CODE_BASEURL_STATEFUL = 'https://stateful.example.com/v1';
mockPrimeCodeFiles.mockClear();
mockCheckCapability.mockResolvedValue(true);
});

afterEach(() => {
delete process.env.LIBRECHAT_CODE_BASEURL_STATEFUL;
delete process.env.LIBRECHAT_CODE_DEFAULT_PROFILE;
});

it('runs an agent with the flag unset stateful when the default profile is stateful', async () => {
process.env.LIBRECHAT_CODE_DEFAULT_PROFILE = 'stateful';
expect(await loadCodeTool({ id: 'agent-1', tools: [Tools.execute_code] })).toBe('stateful');
});

it('runs an agent with the flag false stateful when the default profile is stateful', async () => {
process.env.LIBRECHAT_CODE_DEFAULT_PROFILE = 'stateful';
expect(
await loadCodeTool({
id: 'agent-1',
tools: [Tools.execute_code],
stateful_code_sessions: false,
}),
).toBe('stateful');
});

it('keeps an agent with the flag unset on the default profile when the setting is unset', async () => {
expect(await loadCodeTool({ id: 'agent-1', tools: [Tools.execute_code] })).toBe('default');
});

it('keeps an agent with the flag set stateful when the setting is unset', async () => {
expect(
await loadCodeTool({
id: 'agent-1',
tools: [Tools.execute_code],
stateful_code_sessions: true,
}),
).toBe('stateful');
});

it('does not turn stateful when the capability is off, even with the default profile', async () => {
process.env.LIBRECHAT_CODE_DEFAULT_PROFILE = 'stateful';
mockCheckCapability.mockResolvedValue(false);
expect(await loadCodeTool({ id: 'agent-1', tools: [Tools.execute_code] })).toBe('default');
});
});

describe('MCP credential failure propagation', () => {
const {
OpenIDReauthRequiredError,
Expand Down
12 changes: 11 additions & 1 deletion api/server/routes/files/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const {
verifyAgentUploadPermission,
createCodeExecutionRouteKey,
getCodeExecutionBaseUrl,
getCodeDefaultProfile,
assertUploadContentAllowed,
hasActiveFilePolicy,
sanitizeFilename,
Expand All @@ -27,6 +28,7 @@ const {
} = require('@librechat/api');
const {
isUUID,
AgentCapabilities,
FileSources,
ResourceType,
EModelEndpoint,
Expand All @@ -52,6 +54,7 @@ const { fileAccess } = require('~/server/middleware/accessResources/fileAccess')
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
const { hasCapability } = require('~/server/middleware/roles/capabilities');
const { checkCapability } = require('~/server/services/Config');
const { getRoleByName } = require('~/models');
const { checkPermission } = require('~/server/services/PermissionService');
const { cleanFileName, getContentDisposition } = require('~/server/utils/files');
Expand Down Expand Up @@ -376,7 +379,14 @@ router.get('/code/download/:session_id/:fileId', async (req, res) => {
logger.debug(`${logPrefix} invalid execution_profile`);
return res.status(400).send('Bad request');
}
const executionProfile = requestedProfile ?? 'default';
let executionProfile = requestedProfile ?? getCodeDefaultProfile();
if (
requestedProfile == null &&
executionProfile === 'stateful' &&
!(await checkCapability(req, AgentCapabilities.stateful_code_sessions))
) {
executionProfile = 'default';
}
const requestedRouteKey = req.query.execution_route_key;
if (
requestedRouteKey != null &&
Expand Down
130 changes: 130 additions & 0 deletions api/server/routes/files/files.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ jest.mock('@librechat/api', () => ({
}),
}));

jest.mock('~/server/services/Config', () => ({
checkCapability: jest.fn().mockResolvedValue(false),
}));

jest.mock('~/cache', () => ({
getLogStores: jest.fn(() => ({
get: jest.fn(),
Expand All @@ -74,6 +78,7 @@ jest.mock('~/config', () => ({
const { processDeleteRequest } = require('~/server/services/Files/process');
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { createCodeExecutionRouteKey } = require('@librechat/api');
const { checkCapability } = require('~/server/services/Config');

// Import the router after mocks
const router = require('./files');
Expand Down Expand Up @@ -1651,6 +1656,131 @@ describe('File Routes - Delete with Agent Access', () => {
}
});

describe('with LIBRECHAT_CODE_DEFAULT_PROFILE=stateful', () => {
const sessionId = 's'.repeat(21);
const codeFileId = 'f'.repeat(21);

beforeEach(() => {
process.env.LIBRECHAT_CODE_DEFAULT_PROFILE = 'stateful';
process.env.LIBRECHAT_CODE_BASEURL_STATEFUL = 'https://code-stateful.example.com/v1';
checkCapability.mockResolvedValue(true);
});

afterEach(() => {
delete process.env.LIBRECHAT_CODE_DEFAULT_PROFILE;
delete process.env.LIBRECHAT_CODE_BASEURL_STATEFUL;
checkCapability.mockResolvedValue(false);
});

it('keeps a download with no profile on the default Code API when the capability is off', async () => {
// catches: setting applied without checking the stateful_code_sessions capability
checkCapability.mockResolvedValue(false);
const getDownloadStream = jest.fn().mockResolvedValue({ data: Readable.from(['out']) });
getStrategyFunctions.mockReturnValue({ getDownloadStream });

const response = await request(app).get(`/files/code/download/${sessionId}/${codeFileId}`);

expect(response.status).toBe(200);
expect(getDownloadStream).toHaveBeenCalledWith(
`${sessionId}/${codeFileId}`,
{ kind: 'user', id: otherUserId.toString() },
expect.any(Object),
{ baseUrl: 'https://code-default.example.com/v1', executionProfile: 'default' },
);
});

it('keeps an explicit stateful profile on the stateful Code API when the capability is off', async () => {
// catches: capability gate overriding an explicit profile
checkCapability.mockResolvedValue(false);
const getDownloadStream = jest.fn().mockResolvedValue({ data: Readable.from(['out']) });
getStrategyFunctions.mockReturnValue({ getDownloadStream });

const response = await request(app).get(
`/files/code/download/${sessionId}/${codeFileId}?execution_profile=stateful`,
);

expect(response.status).toBe(200);
expect(getDownloadStream).toHaveBeenCalledWith(
`${sessionId}/${codeFileId}`,
{ kind: 'user', id: otherUserId.toString() },
expect.any(Object),
{ baseUrl: 'https://code-stateful.example.com/v1', executionProfile: 'stateful' },
);
});

it('routes a download with no profile to the stateful Code API', async () => {
// catches: hardcoded 'default' fallback
const getDownloadStream = jest.fn().mockResolvedValue({ data: Readable.from(['out']) });
getStrategyFunctions.mockReturnValue({ getDownloadStream });

const response = await request(app).get(`/files/code/download/${sessionId}/${codeFileId}`);

expect(response.status).toBe(200);
expect(getDownloadStream).toHaveBeenCalledWith(
`${sessionId}/${codeFileId}`,
{ kind: 'user', id: otherUserId.toString() },
expect.any(Object),
{ baseUrl: 'https://code-stateful.example.com/v1', executionProfile: 'stateful' },
);
});

it('keeps an explicit default profile on the default Code API', async () => {
// catches: setting overriding the requested profile instead of only filling the gap
const getDownloadStream = jest.fn().mockResolvedValue({ data: Readable.from(['out']) });
getStrategyFunctions.mockReturnValue({ getDownloadStream });

const response = await request(app).get(
`/files/code/download/${sessionId}/${codeFileId}?execution_profile=default`,
);

expect(response.status).toBe(200);
expect(getDownloadStream).toHaveBeenCalledWith(
`${sessionId}/${codeFileId}`,
{ kind: 'user', id: otherUserId.toString() },
expect.any(Object),
{ baseUrl: 'https://code-default.example.com/v1', executionProfile: 'default' },
);
});

it('still rejects an unknown profile', async () => {
// catches: invalid profile falling through to the setting
const response = await request(app).get(
`/files/code/download/${sessionId}/${codeFileId}?execution_profile=attacker`,
);

expect(response.status).toBe(400);
expect(getStrategyFunctions).not.toHaveBeenCalled();
});

it('accepts a stateful route key with no profile', async () => {
// catches: route-key guard evaluated against the pre-default profile
const response = await request(app).get(
`/files/code/download/${sessionId}/${codeFileId}?execution_route_key=stateful:${'a'.repeat(32)}`,
);

expect(response.status).toBe(404);
expect(getStrategyFunctions).not.toHaveBeenCalled();
});
});

it('keeps a download with no profile on the default Code API when the setting is unset', async () => {
// catches: default flipped without the setting
const getDownloadStream = jest.fn().mockResolvedValue({ data: Readable.from(['out']) });
getStrategyFunctions.mockReturnValue({ getDownloadStream });
const sessionId = 's'.repeat(21);
const codeFileId = 'f'.repeat(21);

const response = await request(app).get(`/files/code/download/${sessionId}/${codeFileId}`);

expect(response.status).toBe(200);
expect(getDownloadStream).toHaveBeenCalledWith(
`${sessionId}/${codeFileId}`,
{ kind: 'user', id: otherUserId.toString() },
expect.any(Object),
{ baseUrl: 'https://code-default.example.com/v1', executionProfile: 'default' },
);
});

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)}`,
Expand Down
4 changes: 4 additions & 0 deletions api/server/routes/files/list.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ jest.mock('~/server/services/PermissionService', () => ({
getEffectivePermissions: jest.fn().mockResolvedValue(0),
}));

jest.mock('~/server/services/Config', () => ({
checkCapability: jest.fn(),
}));

const mockGetLogStores = jest.fn();
jest.mock('~/cache', () => ({
getLogStores: (...args) => mockGetLogStores(...args),
Expand Down
4 changes: 4 additions & 0 deletions api/server/routes/files/preview.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ jest.mock('~/server/services/PermissionService', () => ({
getEffectivePermissions: jest.fn().mockResolvedValue(0),
}));

jest.mock('~/server/services/Config', () => ({
checkCapability: jest.fn(),
}));

jest.mock('~/server/utils/files', () => ({
cleanFileName: (name) => name,
getContentDisposition: (name) => `attachment; filename="${name}"`,
Expand Down
62 changes: 62 additions & 0 deletions api/server/services/Endpoints/agents/addedConvo.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -361,3 +361,65 @@ describe('processAddedConvo', () => {
expect(mockGetSkillDbMethods).toHaveBeenCalledTimes(1);
});
});

describe('processAddedConvo — LIBRECHAT_CODE_DEFAULT_PROFILE', () => {
beforeEach(() => {
jest.clearAllMocks();
mockValidateAgentModel.mockResolvedValue({ isValid: true });
mockInitializeAgent.mockResolvedValue({ id: 'added-agent', userMCPAuthMap: undefined });
mockResolveAgentScopedSkillIds.mockReturnValue([]);
mockResolveModelSpecSkillIds.mockResolvedValue([]);
mockCanAuthorSkillFiles.mockReturnValue(false);
mockGetSkillDbMethods.mockReturnValue({
getSkillByName: mockRegistryGetSkillByName,
listSkillsByAccess: mockRegistryListSkillsByAccess,
listAlwaysApplySkills: mockRegistryListAlwaysApplySkills,
});
});

afterEach(() => {
delete process.env.LIBRECHAT_CODE_DEFAULT_PROFILE;
});

const savedAgent = () => ({
id: 'added-agent',
provider: 'openai',
tools: ['execute_code'],
stateful_code_sessions: false,
});

it.each([true, false])(
'hands initializeAgent the saved toggle-off agent untouched with statefulSessionsAvailable=%s so the setting decides',
async (statefulSessionsAvailable) => {
process.env.LIBRECHAT_CODE_DEFAULT_PROFILE = 'stateful';
const agent = savedAgent();
mockLoadAddedAgent.mockResolvedValue(agent);

await processAddedConvo({
req: makeReq(),
res: {},
endpointOption: { addedConvo: { model: 'gpt-4o', agent_id: 'added-agent' } },
modelsConfig: { openai: ['gpt-4o'] },
logViolation: jest.fn(),
loadTools: jest.fn(),
requestFiles: [],
conversationId: 'conv-1',
parentMessageId: null,
allowedProviders: new Set(['openai']),
agentConfigs: new Map(),
primaryAgentId: 'primary-id',
primaryAgent: { id: 'primary-id' },
userMCPAuthMap: undefined,
codeEnvAvailable: true,
statefulSessionsAvailable,
});

const [params] = mockInitializeAgent.mock.calls[0];
expect(params.agent).toBe(agent);
expect(params.agent.stateful_code_sessions).toBe(false);
expect(params.agent.tools).toEqual(['execute_code']);
expect(params.codeEnvAvailable).toBe(true);
expect(params.statefulSessionsAvailable).toBe(statefulSessionsAvailable);
},
);
});
Loading