From bc2c19d8416e9bd0f7c199fee0279ec5c9fb7449 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 12 May 2026 21:37:57 -0400 Subject: [PATCH] fix: unset empty Langfuse agent config --- api/server/controllers/agents/v1.js | 12 +++++++-- api/server/controllers/agents/v1.spec.js | 30 ++++++++++++++++++++++ packages/api/src/agents/langfuse.spec.ts | 13 ++++++++++ packages/api/src/agents/langfuse.ts | 2 +- packages/data-schemas/src/methods/agent.ts | 30 +++++++++++++++++++--- 5 files changed, 80 insertions(+), 7 deletions(-) diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js index f6b61521f2..59a761aa70 100644 --- a/api/server/controllers/agents/v1.js +++ b/api/server/controllers/agents/v1.js @@ -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, + }; + } } } diff --git a/api/server/controllers/agents/v1.spec.js b/api/server/controllers/agents/v1.spec.js index a0e54932fb..2dac7a3eaa 100644 --- a/api/server/controllers/agents/v1.spec.js +++ b/api/server/controllers/agents/v1.spec.js @@ -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 }, diff --git a/packages/api/src/agents/langfuse.spec.ts b/packages/api/src/agents/langfuse.spec.ts index 869ec8ba81..85f2a60371 100644 --- a/packages/api/src/agents/langfuse.spec.ts +++ b/packages/api/src/agents/langfuse.spec.ts @@ -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( { diff --git a/packages/api/src/agents/langfuse.ts b/packages/api/src/agents/langfuse.ts index 14b9adc639..3308686a3c 100644 --- a/packages/api/src/agents/langfuse.ts +++ b/packages/api/src/agents/langfuse.ts @@ -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)) { diff --git a/packages/data-schemas/src/methods/agent.ts b/packages/data-schemas/src/methods/agent.ts index 833f0b9f63..5c5785767b 100644 --- a/packages/data-schemas/src/methods/agent.ts +++ b/packages/data-schemas/src/methods/agent.ts @@ -57,6 +57,15 @@ function extractMCPServerNames(tools: string[] | undefined | null): string[] { return Array.from(serverNames); } +function removeUnsetFields(target: Record, 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; + removeUnsetFields(wouldBeVersion, $unset); const lastVersion = versions[versions.length - 1] as Record; 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; - const { $push, $pull, $addToSet, ...directUpdates } = updateData; + const { $push, $pull, $addToSet, $unset, ...directUpdates } = updateData; // Sync mcpServerNames when tools are updated if ((directUpdates as Record).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;