fix: unset empty Langfuse agent config

This commit is contained in:
Danny Avila 2026-05-12 21:37:57 -04:00
parent ddb7090d64
commit bc2c19d841
5 changed files with 80 additions and 7 deletions

View file

@ -566,12 +566,20 @@ const updateAgentHandler = async (req, res) => {
}
if (updateData.langfuse) {
updateData.langfuse = await normalizeLangfuseConfig(
const normalizedLangfuse = await normalizeLangfuseConfig(
updateData.langfuse,
existingAgent.langfuse,
);
if (!updateData.langfuse) {
if (normalizedLangfuse) {
updateData.langfuse = normalizedLangfuse;
} else {
delete updateData.langfuse;
if (existingAgent.langfuse) {
updateData.$unset = {
...(updateData.$unset || {}),
langfuse: 1,
};
}
}
}

View file

@ -958,6 +958,36 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
});
});
test('should remove Langfuse config when update clears the only stored field', async () => {
const encryptedOriginal = await encryptStoredSecret('sk-original');
await Agent.updateOne(
{ id: existingAgentId },
{
langfuse: {
secretKey: encryptedOriginal,
},
},
);
mockReq.params.id = existingAgentId;
mockReq.body = {
langfuse: {
secretKey: LANGFUSE_SECRET_CLEAR_VALUE,
},
};
await updateAgentHandler(mockReq, mockRes);
expect(mockRes.json).toHaveBeenCalled();
const updatedAgent = mockRes.json.mock.calls[0][0];
expect(updatedAgent.langfuse).toBeUndefined();
const agentInDb = await Agent.findOne({ id: existingAgentId }).lean();
expect(agentInDb.langfuse).toBeUndefined();
const latestVersion = agentInDb.versions[agentInDb.versions.length - 1];
expect(latestVersion.langfuse).toBeUndefined();
});
test('uploadAgentAvatarHandler should redact Langfuse secret in response', async () => {
await Agent.updateOne(
{ id: existingAgentId },

View file

@ -59,6 +59,19 @@ describe('normalizeLangfuseConfig', () => {
});
});
it('returns undefined when clearing the only stored field', async () => {
const result = await normalizeLangfuseConfig(
{
secretKey: LANGFUSE_SECRET_CLEAR_VALUE,
},
{
secretKey: '0123456789abcdef0123456789abcdef:736b2d6167656e74',
},
);
expect(result).toBeUndefined();
});
it('clears explicit blank non-secret fields while preserving absent fields', async () => {
const result = await normalizeLangfuseConfig(
{

View file

@ -84,7 +84,7 @@ export async function normalizeLangfuseConfig(
const incomingSecret = incoming.secretKey;
if (incomingSecret === LANGFUSE_SECRET_CLEAR_VALUE) {
return normalized;
return Object.keys(normalized).length > 0 ? normalized : undefined;
}
if (isNonEmptyString(incomingSecret)) {

View file

@ -57,6 +57,15 @@ function extractMCPServerNames(tools: string[] | undefined | null): string[] {
return Array.from(serverNames);
}
function removeUnsetFields(target: Record<string, unknown>, unsetUpdates: unknown): void {
if (!unsetUpdates || typeof unsetUpdates !== 'object' || Array.isArray(unsetUpdates)) {
return;
}
for (const key of Object.keys(unsetUpdates)) {
delete target[key];
}
}
/**
* Check if a version already exists in the versions array, excluding timestamp and author fields.
*/
@ -84,13 +93,20 @@ 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 && !$unset && !actionsHash) {
return null;
}
const wouldBeVersion = { ...currentData, ...directUpdates } as Record<string, unknown>;
removeUnsetFields(wouldBeVersion, $unset);
const lastVersion = versions[versions.length - 1] as Record<string, unknown>;
if (actionsHash && lastVersion.actionsHash !== actionsHash) {
@ -310,7 +326,7 @@ export function createAgentMethods(mongoose: typeof import('mongoose'), deps: Ag
author: _author,
...versionData
} = currentAgent.toObject() as unknown as Record<string, unknown>;
const { $push, $pull, $addToSet, ...directUpdates } = updateData;
const { $push, $pull, $addToSet, $unset, ...directUpdates } = updateData;
// Sync mcpServerNames when tools are updated
if ((directUpdates as Record<string, unknown>).tools !== undefined) {
@ -348,7 +364,12 @@ export function createAgentMethods(mongoose: typeof import('mongoose'), deps: Ag
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(
@ -372,6 +393,7 @@ export function createAgentMethods(mongoose: typeof import('mongoose'), deps: Ag
...directUpdates,
updatedAt: new Date(),
};
removeUnsetFields(versionEntry, $unset);
if (actionsHash) {
versionEntry.actionsHash = actionsHash;