From bed2ab921080d9be7cff059bed8d564bc17f9afa Mon Sep 17 00:00:00 2001 From: Yen Su <49794855+yens1@users.noreply.github.com> Date: Wed, 21 Oct 2026 08:50:00 +0000 Subject: [PATCH] fix(Databricks AI Agent Node): Scrub MLflow trace request and response previews (#41247) --- .../Databricks/mlflow/__tests__/trace-writer.test.ts | 12 ++++++++++++ .../nodes/vendors/Databricks/mlflow/trace-writer.ts | 7 +++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/@n8n/nodes-langchain/nodes/vendors/Databricks/mlflow/__tests__/trace-writer.test.ts b/packages/@n8n/nodes-langchain/nodes/vendors/Databricks/mlflow/__tests__/trace-writer.test.ts index 1dacb08724c..9ae1fa49657 100644 --- a/packages/@n8n/nodes-langchain/nodes/vendors/Databricks/mlflow/__tests__/trace-writer.test.ts +++ b/packages/@n8n/nodes-langchain/nodes/vendors/Databricks/mlflow/__tests__/trace-writer.test.ts @@ -355,4 +355,16 @@ describe('previews', () => { expect(info.response_preview).toContain('25 days'); }); + + it('removes credential material from the plain-text previews, even across the cap', () => { + const token = ['dapi', '0123456789abcdef0123456789abcdef'].join(''); + // The token straddles the 1,000-character cut, so a slice before the scrub + // would leave a partial token in the preview. + const withToken = { ...trace(), requestPreview: `${'x'.repeat(989)} ${token}` }; + + const info = buildTraceInfo(withToken, '42'); + + expect(info.request_preview).toContain('[REDACTED]'); + expect(info.request_preview).not.toContain(token); + }); }); diff --git a/packages/@n8n/nodes-langchain/nodes/vendors/Databricks/mlflow/trace-writer.ts b/packages/@n8n/nodes-langchain/nodes/vendors/Databricks/mlflow/trace-writer.ts index 877a40bbd6e..423640ca32f 100644 --- a/packages/@n8n/nodes-langchain/nodes/vendors/Databricks/mlflow/trace-writer.ts +++ b/packages/@n8n/nodes-langchain/nodes/vendors/Databricks/mlflow/trace-writer.ts @@ -1,5 +1,6 @@ import { isRecord } from '@n8n/utils/is-record'; import { sanitizeErrorDetail } from '@n8n/utils/redaction/sanitize-error-detail'; +import { scrubSecretsInText } from '@n8n/utils/scrub-secrets'; import { OperationalError, UnexpectedError, UserError, type IDataObject } from 'n8n-workflow'; import type { CollectedTrace, MlflowSpan } from './types'; @@ -140,14 +141,16 @@ export async function ensureExperiment( /** * The previews are the two columns of the trace list, so they carry the question * and the answer as plain text. The serialized span attribute is the fallback - * for a run that ended before the agent produced either. + * for a run that ended before the agent produced either. The plain text is raw + * chain input, so it is scrubbed here, before the cut, so a token that straddles + * the limit cannot survive as a partial match. */ function preview( text: string | undefined, span: MlflowSpan | undefined, attribute: string, ): string { - return (text ?? span?.attributes[attribute] ?? '').slice(0, MAX_PREVIEW_CHARS); + return scrubSecretsInText(text ?? span?.attributes[attribute] ?? '').slice(0, MAX_PREVIEW_CHARS); } /**