Skip to content

Commit a62b95a

Browse files
authored
🧑‍💻 fix: Agents Config Defaults and Avatar Uploads Across File Strategies (danny-avila#7814)
* fix: avatar processing across storage services, uniqueness by agent ID, prevent overwriting user avatar * fix: sanitize file paths in deleteLocalFile function to prevent invalid path errors * fix: correct spelling of 'agentsEndpointSchema' in agents.js and config.ts * fix: default app.locals agents configuration setup and add agent endpoint schema default
1 parent 7d3fde0 commit a62b95a

File tree

13 files changed

+192
-55
lines changed

13 files changed

+192
-55
lines changed

‎api/server/controllers/agents/v1.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ const uploadAgentAvatarHandler = async (req, res) => {
387387
buffer: resizedBuffer,
388388
userId: req.user.id,
389389
manual: 'false',
390+
agentId: agent_id,
390391
});
391392

392393
const image = {

‎api/server/services/AppService.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const {
77
getConfigDefaults,
88
loadWebSearchConfig,
99
} = require('librechat-data-provider');
10+
const { agentsConfigSetup } = require('@librechat/api');
1011
const {
1112
checkHealth,
1213
checkConfig,
@@ -25,7 +26,6 @@ const { azureConfigSetup } = require('./start/azureOpenAI');
2526
const { processModelSpecs } = require('./start/modelSpecs');
2627
const { initializeS3 } = require('./Files/S3/initialize');
2728
const { loadAndFormatTools } = require('./ToolService');
28-
const { agentsConfigSetup } = require('./start/agents');
2929
const { isEnabled } = require('~/server/utils');
3030
const { initializeRoles } = require('~/models');
3131
const { getMCPManager } = require('~/config');
@@ -103,8 +103,13 @@ const AppService = async (app) => {
103103
balance,
104104
};
105105

106+
const agentsDefaults = agentsConfigSetup(config);
107+
106108
if (!Object.keys(config).length) {
107-
app.locals = defaultLocals;
109+
app.locals = {
110+
...defaultLocals,
111+
[EModelEndpoint.agents]: agentsDefaults,
112+
};
108113
return;
109114
}
110115

@@ -139,9 +144,7 @@ const AppService = async (app) => {
139144
);
140145
}
141146

142-
if (endpoints?.[EModelEndpoint.agents]) {
143-
endpointLocals[EModelEndpoint.agents] = agentsConfigSetup(config);
144-
}
147+
endpointLocals[EModelEndpoint.agents] = agentsConfigSetup(config, agentsDefaults);
145148

146149
const endpointKeys = [
147150
EModelEndpoint.openAI,

‎api/server/services/AppService.spec.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ const {
22
FileSources,
33
EModelEndpoint,
44
EImageOutputType,
5+
AgentCapabilities,
56
defaultSocialLogins,
67
validateAzureGroups,
8+
defaultAgentCapabilities,
79
deprecatedAzureVariables,
810
conflictingAzureVariables,
911
} = require('librechat-data-provider');
@@ -151,6 +153,11 @@ describe('AppService', () => {
151153
safeSearch: 1,
152154
serperApiKey: '${SERPER_API_KEY}',
153155
},
156+
memory: undefined,
157+
agents: {
158+
disableBuilder: false,
159+
capabilities: expect.arrayContaining([...defaultAgentCapabilities]),
160+
},
154161
});
155162
});
156163

@@ -268,6 +275,71 @@ describe('AppService', () => {
268275
);
269276
});
270277

278+
it('should correctly configure Agents endpoint based on custom config', async () => {
279+
require('./Config/loadCustomConfig').mockImplementationOnce(() =>
280+
Promise.resolve({
281+
endpoints: {
282+
[EModelEndpoint.agents]: {
283+
disableBuilder: true,
284+
recursionLimit: 10,
285+
maxRecursionLimit: 20,
286+
allowedProviders: ['openai', 'anthropic'],
287+
capabilities: [AgentCapabilities.tools, AgentCapabilities.actions],
288+
},
289+
},
290+
}),
291+
);
292+
293+
await AppService(app);
294+
295+
expect(app.locals).toHaveProperty(EModelEndpoint.agents);
296+
expect(app.locals[EModelEndpoint.agents]).toEqual(
297+
expect.objectContaining({
298+
disableBuilder: true,
299+
recursionLimit: 10,
300+
maxRecursionLimit: 20,
301+
allowedProviders: expect.arrayContaining(['openai', 'anthropic']),
302+
capabilities: expect.arrayContaining([AgentCapabilities.tools, AgentCapabilities.actions]),
303+
}),
304+
);
305+
});
306+
307+
it('should configure Agents endpoint with defaults when no config is provided', async () => {
308+
require('./Config/loadCustomConfig').mockImplementationOnce(() => Promise.resolve({}));
309+
310+
await AppService(app);
311+
312+
expect(app.locals).toHaveProperty(EModelEndpoint.agents);
313+
expect(app.locals[EModelEndpoint.agents]).toEqual(
314+
expect.objectContaining({
315+
disableBuilder: false,
316+
capabilities: expect.arrayContaining([...defaultAgentCapabilities]),
317+
}),
318+
);
319+
});
320+
321+
it('should configure Agents endpoint with defaults when endpoints exist but agents is not defined', async () => {
322+
require('./Config/loadCustomConfig').mockImplementationOnce(() =>
323+
Promise.resolve({
324+
endpoints: {
325+
[EModelEndpoint.openAI]: {
326+
titleConvo: true,
327+
},
328+
},
329+
}),
330+
);
331+
332+
await AppService(app);
333+
334+
expect(app.locals).toHaveProperty(EModelEndpoint.agents);
335+
expect(app.locals[EModelEndpoint.agents]).toEqual(
336+
expect.objectContaining({
337+
disableBuilder: false,
338+
capabilities: expect.arrayContaining([...defaultAgentCapabilities]),
339+
}),
340+
);
341+
});
342+
271343
it('should correctly configure minimum Azure OpenAI Assistant values', async () => {
272344
const assistantGroups = [azureGroups[0], { ...azureGroups[1], assistants: true }];
273345
require('./Config/loadCustomConfig').mockImplementationOnce(() =>

‎api/server/services/Files/Azure/images.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,15 +91,28 @@ async function prepareAzureImageURL(req, file) {
9191
* @param {Buffer} params.buffer - The avatar image buffer.
9292
* @param {string} params.userId - The user's id.
9393
* @param {string} params.manual - Flag to indicate manual update.
94+
* @param {string} [params.agentId] - Optional agent ID if this is an agent avatar.
9495
* @param {string} [params.basePath='images'] - The base folder within the container.
9596
* @param {string} [params.containerName] - The Azure Blob container name.
9697
* @returns {Promise<string>} The URL of the avatar.
9798
*/
98-
async function processAzureAvatar({ buffer, userId, manual, basePath = 'images', containerName }) {
99+
async function processAzureAvatar({
100+
buffer,
101+
userId,
102+
manual,
103+
agentId,
104+
basePath = 'images',
105+
containerName,
106+
}) {
99107
try {
100108
const metadata = await sharp(buffer).metadata();
101109
const extension = metadata.format === 'gif' ? 'gif' : 'png';
102-
const fileName = `avatar.${extension}`;
110+
const timestamp = new Date().getTime();
111+
112+
/** Unique filename with timestamp and optional agent ID */
113+
const fileName = agentId
114+
? `agent-${agentId}-avatar-${timestamp}.${extension}`
115+
: `avatar-${timestamp}.${extension}`;
103116

104117
const downloadURL = await saveBufferToAzure({
105118
userId,
@@ -110,9 +123,12 @@ async function processAzureAvatar({ buffer, userId, manual, basePath = 'images',
110123
});
111124
const isManual = manual === 'true';
112125
const url = `${downloadURL}?manual=${isManual}`;
113-
if (isManual) {
126+
127+
// Only update user record if this is a user avatar (manual === 'true')
128+
if (isManual && !agentId) {
114129
await updateUser(userId, { avatar: url });
115130
}
131+
116132
return url;
117133
} catch (error) {
118134
logger.error('[processAzureAvatar] Error uploading profile picture to Azure:', error);

‎api/server/services/Files/Firebase/images.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,20 @@ async function prepareImageURL(req, file) {
8282
* @param {Buffer} params.buffer - The Buffer containing the avatar image.
8383
* @param {string} params.userId - The user ID.
8484
* @param {string} params.manual - A string flag indicating whether the update is manual ('true' or 'false').
85+
* @param {string} [params.agentId] - Optional agent ID if this is an agent avatar.
8586
* @returns {Promise<string>} - A promise that resolves with the URL of the uploaded avatar.
8687
* @throws {Error} - Throws an error if Firebase is not initialized or if there is an error in uploading.
8788
*/
88-
async function processFirebaseAvatar({ buffer, userId, manual }) {
89+
async function processFirebaseAvatar({ buffer, userId, manual, agentId }) {
8990
try {
9091
const metadata = await sharp(buffer).metadata();
9192
const extension = metadata.format === 'gif' ? 'gif' : 'png';
92-
const fileName = `avatar.${extension}`;
93+
const timestamp = new Date().getTime();
94+
95+
/** Unique filename with timestamp and optional agent ID */
96+
const fileName = agentId
97+
? `agent-${agentId}-avatar-${timestamp}.${extension}`
98+
: `avatar-${timestamp}.${extension}`;
9399

94100
const downloadURL = await saveBufferToFirebase({
95101
userId,
@@ -98,10 +104,10 @@ async function processFirebaseAvatar({ buffer, userId, manual }) {
98104
});
99105

100106
const isManual = manual === 'true';
101-
102107
const url = `${downloadURL}?manual=${isManual}`;
103108

104-
if (isManual) {
109+
// Only update user record if this is a user avatar (manual === 'true')
110+
if (isManual && !agentId) {
105111
await updateUser(userId, { avatar: url });
106112
}
107113

‎api/server/services/Files/Local/crud.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,10 @@ const unlinkFile = async (filepath) => {
201201
*/
202202
const deleteLocalFile = async (req, file) => {
203203
const { publicPath, uploads } = req.app.locals.paths;
204+
205+
/** Filepath stripped of query parameters (e.g., ?manual=true) */
206+
const cleanFilepath = file.filepath.split('?')[0];
207+
204208
if (file.embedded && process.env.RAG_API_URL) {
205209
const jwtToken = req.headers.authorization.split(' ')[1];
206210
axios.delete(`${process.env.RAG_API_URL}/documents`, {
@@ -213,32 +217,32 @@ const deleteLocalFile = async (req, file) => {
213217
});
214218
}
215219

216-
if (file.filepath.startsWith(`/uploads/${req.user.id}`)) {
220+
if (cleanFilepath.startsWith(`/uploads/${req.user.id}`)) {
217221
const userUploadDir = path.join(uploads, req.user.id);
218-
const basePath = file.filepath.split(`/uploads/${req.user.id}/`)[1];
222+
const basePath = cleanFilepath.split(`/uploads/${req.user.id}/`)[1];
219223

220224
if (!basePath) {
221-
throw new Error(`Invalid file path: ${file.filepath}`);
225+
throw new Error(`Invalid file path: ${cleanFilepath}`);
222226
}
223227

224228
const filepath = path.join(userUploadDir, basePath);
225229

226230
const rel = path.relative(userUploadDir, filepath);
227231
if (rel.startsWith('..') || path.isAbsolute(rel) || rel.includes(`..${path.sep}`)) {
228-
throw new Error(`Invalid file path: ${file.filepath}`);
232+
throw new Error(`Invalid file path: ${cleanFilepath}`);
229233
}
230234

231235
await unlinkFile(filepath);
232236
return;
233237
}
234238

235-
const parts = file.filepath.split(path.sep);
239+
const parts = cleanFilepath.split(path.sep);
236240
const subfolder = parts[1];
237241
if (!subfolder && parts[0] === EModelEndpoint.agents) {
238242
logger.warn(`Agent File ${file.file_id} is missing filepath, may have been deleted already`);
239243
return;
240244
}
241-
const filepath = path.join(publicPath, file.filepath);
245+
const filepath = path.join(publicPath, cleanFilepath);
242246

243247
if (!isValidPath(req, publicPath, subfolder, filepath)) {
244248
throw new Error('Invalid file path');

‎api/server/services/Files/Local/images.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,11 @@ async function prepareImagesLocal(req, file) {
112112
* @param {Buffer} params.buffer - The Buffer containing the avatar image.
113113
* @param {string} params.userId - The user ID.
114114
* @param {string} params.manual - A string flag indicating whether the update is manual ('true' or 'false').
115+
* @param {string} [params.agentId] - Optional agent ID if this is an agent avatar.
115116
* @returns {Promise<string>} - A promise that resolves with the URL of the uploaded avatar.
116117
* @throws {Error} - Throws an error if Firebase is not initialized or if there is an error in uploading.
117118
*/
118-
async function processLocalAvatar({ buffer, userId, manual }) {
119+
async function processLocalAvatar({ buffer, userId, manual, agentId }) {
119120
const userDir = path.resolve(
120121
__dirname,
121122
'..',
@@ -132,7 +133,11 @@ async function processLocalAvatar({ buffer, userId, manual }) {
132133
const metadata = await sharp(buffer).metadata();
133134
const extension = metadata.format === 'gif' ? 'gif' : 'png';
134135

135-
const fileName = `avatar-${new Date().getTime()}.${extension}`;
136+
const timestamp = new Date().getTime();
137+
/** Unique filename with timestamp and optional agent ID */
138+
const fileName = agentId
139+
? `agent-${agentId}-avatar-${timestamp}.${extension}`
140+
: `avatar-${timestamp}.${extension}`;
136141
const urlRoute = `/images/${userId}/${fileName}`;
137142
const avatarPath = path.join(userDir, fileName);
138143

@@ -142,7 +147,8 @@ async function processLocalAvatar({ buffer, userId, manual }) {
142147
const isManual = manual === 'true';
143148
let url = `${urlRoute}?manual=${isManual}`;
144149

145-
if (isManual) {
150+
// Only update user record if this is a user avatar (manual === 'true')
151+
if (isManual && !agentId) {
146152
await updateUser(userId, { avatar: url });
147153
}
148154

‎api/server/services/Files/S3/images.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,19 +94,28 @@ async function prepareImageURLS3(req, file) {
9494
* @param {Buffer} params.buffer - Avatar image buffer.
9595
* @param {string} params.userId - User's unique identifier.
9696
* @param {string} params.manual - 'true' or 'false' flag for manual update.
97+
* @param {string} [params.agentId] - Optional agent ID if this is an agent avatar.
9798
* @param {string} [params.basePath='images'] - Base path in the bucket.
9899
* @returns {Promise<string>} Signed URL of the uploaded avatar.
99100
*/
100-
async function processS3Avatar({ buffer, userId, manual, basePath = defaultBasePath }) {
101+
async function processS3Avatar({ buffer, userId, manual, agentId, basePath = defaultBasePath }) {
101102
try {
102103
const metadata = await sharp(buffer).metadata();
103104
const extension = metadata.format === 'gif' ? 'gif' : 'png';
104-
const fileName = `avatar.${extension}`;
105+
const timestamp = new Date().getTime();
106+
107+
/** Unique filename with timestamp and optional agent ID */
108+
const fileName = agentId
109+
? `agent-${agentId}-avatar-${timestamp}.${extension}`
110+
: `avatar-${timestamp}.${extension}`;
105111

106112
const downloadURL = await saveBufferToS3({ userId, buffer, fileName, basePath });
107-
if (manual === 'true') {
113+
114+
// Only update user record if this is a user avatar (manual === 'true')
115+
if (manual === 'true' && !agentId) {
108116
await updateUser(userId, { avatar: downloadURL });
109117
}
118+
110119
return downloadURL;
111120
} catch (error) {
112121
logger.error('[processS3Avatar] Error processing S3 avatar:', error.message);

‎api/server/services/start/agents.js

Lines changed: 0 additions & 14 deletions
This file was deleted.

‎api/strategies/process.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const handleExistingUser = async (oldUser, avatarUrl) => {
3131
input: avatarUrl,
3232
});
3333
const { processAvatar } = getStrategyFunctions(fileStrategy);
34-
updatedAvatar = await processAvatar({ buffer: resizedBuffer, userId });
34+
updatedAvatar = await processAvatar({ buffer: resizedBuffer, userId, manual: 'false' });
3535
}
3636

3737
if (updatedAvatar) {
@@ -90,7 +90,11 @@ const createSocialUser = async ({
9090
input: avatarUrl,
9191
});
9292
const { processAvatar } = getStrategyFunctions(fileStrategy);
93-
const avatar = await processAvatar({ buffer: resizedBuffer, userId: newUserId });
93+
const avatar = await processAvatar({
94+
buffer: resizedBuffer,
95+
userId: newUserId,
96+
manual: 'false',
97+
});
9498
await updateUser(newUserId, { avatar });
9599
}
96100

0 commit comments

Comments
 (0)