mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-09-01 03:27:01 +00:00
fix: harden Langfuse agent config handling
This commit is contained in:
parent
0c2fdc92cd
commit
bc636c9a2e
18 changed files with 628 additions and 149 deletions
|
|
@ -1,12 +1,14 @@
|
|||
const { z } = require('zod');
|
||||
const fs = require('fs').promises;
|
||||
const { nanoid } = require('nanoid');
|
||||
const { logger, encryptV2 } = require('@librechat/data-schemas');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const {
|
||||
refreshS3Url,
|
||||
agentCreateSchema,
|
||||
agentUpdateSchema,
|
||||
redactLangfuseSecret,
|
||||
refreshListAvatars,
|
||||
normalizeLangfuseConfig,
|
||||
collectEdgeAgentIds,
|
||||
mergeAgentOcrConversion,
|
||||
MAX_AVATAR_REFRESH_AGENTS,
|
||||
|
|
@ -60,88 +62,6 @@ const getSafeModelParameters = (modelParameters) => {
|
|||
return typeof useResponsesApi === 'boolean' ? { useResponsesApi } : {};
|
||||
};
|
||||
|
||||
const toPlainObject = (value) =>
|
||||
value && typeof value.toObject === 'function' ? value.toObject() : value;
|
||||
|
||||
const isNonEmptyString = (value) => typeof value === 'string' && value.trim().length > 0;
|
||||
const ENCRYPTED_V2_VALUE = /^[a-f0-9]{32}:[a-f0-9]+$/i;
|
||||
|
||||
const encryptSensitiveValue = async (value) => encryptV2(encodeURIComponent(value));
|
||||
|
||||
const normalizeLangfuseSecret = async (value, options = {}) => {
|
||||
if (!isNonEmptyString(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (options.preserveEncrypted === true && ENCRYPTED_V2_VALUE.test(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
return await encryptSensitiveValue(trimmed);
|
||||
};
|
||||
|
||||
const normalizeLangfuseConfig = async (incoming, existing, options = {}) => {
|
||||
if (!incoming || typeof incoming !== 'object' || Array.isArray(incoming)) {
|
||||
return incoming;
|
||||
}
|
||||
|
||||
const existingConfig = toPlainObject(existing) ?? {};
|
||||
const normalized = {};
|
||||
|
||||
if (typeof incoming.enabled === 'boolean') {
|
||||
normalized.enabled = incoming.enabled;
|
||||
}
|
||||
|
||||
for (const key of ['publicKey', 'baseUrl']) {
|
||||
if (isNonEmptyString(incoming[key])) {
|
||||
normalized[key] = incoming[key].trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (isNonEmptyString(incoming.secretKey)) {
|
||||
normalized.secretKey = await normalizeLangfuseSecret(incoming.secretKey, {
|
||||
preserveEncrypted: options.preserveIncomingEncrypted === true,
|
||||
});
|
||||
} else if (isNonEmptyString(existingConfig.secretKey)) {
|
||||
normalized.secretKey = await normalizeLangfuseSecret(existingConfig.secretKey, {
|
||||
preserveEncrypted: true,
|
||||
});
|
||||
}
|
||||
|
||||
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
||||
};
|
||||
|
||||
const redactLangfuseSecret = (agent) => {
|
||||
const payload = toPlainObject(agent);
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return payload;
|
||||
}
|
||||
|
||||
const redactSingleAgent = (value) => {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return value;
|
||||
}
|
||||
if (value.langfuse && typeof value.langfuse === 'object' && value.langfuse.secretKey) {
|
||||
return {
|
||||
...value,
|
||||
langfuse: {
|
||||
...toPlainObject(value.langfuse),
|
||||
secretKey: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const redactedPayload = redactSingleAgent(payload);
|
||||
if (Array.isArray(redactedPayload.versions)) {
|
||||
redactedPayload.versions = redactedPayload.versions.map((version) =>
|
||||
redactSingleAgent(toPlainObject(version)),
|
||||
);
|
||||
}
|
||||
|
||||
return redactedPayload;
|
||||
};
|
||||
|
||||
/**
|
||||
* Looks up each referenced agent id in Mongo, splits them into three
|
||||
* buckets the caller needs for validation: ids that don't exist at all,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,11 @@ const fs = require('fs').promises;
|
|||
const { nanoid } = require('nanoid');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const { agentSchema, fileSchema, encryptV2, decryptV2 } = require('@librechat/data-schemas');
|
||||
const { FileSources, PermissionBits } = require('librechat-data-provider');
|
||||
const {
|
||||
FileSources,
|
||||
PermissionBits,
|
||||
LANGFUSE_SECRET_CLEAR_VALUE,
|
||||
} = require('librechat-data-provider');
|
||||
const { MongoMemoryServer } = require('mongodb-memory-server');
|
||||
|
||||
// Only mock the dependencies that are not database-related
|
||||
|
|
@ -877,6 +881,44 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
|
|||
expect(await decryptStoredSecret(agentInDb.langfuse.secretKey)).toBe('sk-updated');
|
||||
});
|
||||
|
||||
test('should clear existing Langfuse secret when update sends the clear sentinel', async () => {
|
||||
const encryptedOriginal = await encryptStoredSecret('sk-original');
|
||||
await Agent.updateOne(
|
||||
{ id: existingAgentId },
|
||||
{
|
||||
langfuse: {
|
||||
enabled: true,
|
||||
publicKey: 'pk-original',
|
||||
secretKey: encryptedOriginal,
|
||||
baseUrl: 'https://cloud.langfuse.com',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
mockReq.params.id = existingAgentId;
|
||||
mockReq.body = {
|
||||
langfuse: {
|
||||
enabled: true,
|
||||
publicKey: 'pk-original',
|
||||
secretKey: LANGFUSE_SECRET_CLEAR_VALUE,
|
||||
baseUrl: 'https://cloud.langfuse.com',
|
||||
},
|
||||
};
|
||||
|
||||
await updateAgentHandler(mockReq, mockRes);
|
||||
|
||||
expect(mockRes.json).toHaveBeenCalled();
|
||||
const updatedAgent = mockRes.json.mock.calls[0][0];
|
||||
expect(updatedAgent.langfuse.secretKey).toBeUndefined();
|
||||
|
||||
const agentInDb = await Agent.findOne({ id: existingAgentId }).lean();
|
||||
expect(agentInDb.langfuse).toEqual({
|
||||
enabled: true,
|
||||
publicKey: 'pk-original',
|
||||
baseUrl: 'https://cloud.langfuse.com',
|
||||
});
|
||||
});
|
||||
|
||||
test('uploadAgentAvatarHandler should redact Langfuse secret in response', async () => {
|
||||
await Agent.updateOne(
|
||||
{ id: existingAgentId },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue