diff --git a/packages/api/src/agents/__tests__/messagePiiFilter.spec.ts b/packages/api/src/agents/__tests__/messagePiiFilter.spec.ts index dadecde599..d8a6ae7030 100644 --- a/packages/api/src/agents/__tests__/messagePiiFilter.spec.ts +++ b/packages/api/src/agents/__tests__/messagePiiFilter.spec.ts @@ -47,7 +47,8 @@ describe('createMessagePiiFilterHooks', () => { expect(result.updatedPrompt).toBe('key sk-[REDACTED] please'); expect(result.decision).toBeUndefined(); - expect(built!.collector.matches).toHaveLength(1); + // Silent mode skips collector population (no consumer reads it) + expect(built!.collector.matches).toEqual([]); }); it('is a no-op when nothing matches', async () => { @@ -110,7 +111,7 @@ describe('createMessagePiiFilterHooks', () => { expect(result.updatedPrompt).toBe('auth Bearer [REDACTED] and key sk-ant-1234567890ABC'); }); - it('layers customPatterns on top of starters', async () => { + it('uses customPatterns only when starterPatterns is empty', async () => { const built = createMessagePiiFilterHooks( silent({ starterPatterns: [], @@ -123,7 +124,25 @@ describe('createMessagePiiFilterHooks', () => { }); expect(result.updatedPrompt).toBe('token [REDACTED] ok'); - expect(built!.collector.matches.map((m) => m.patternId)).toEqual(['acme']); + }); + + it('layers customPatterns on top of starters (both fire in one prompt)', async () => { + const built = createMessagePiiFilterHooks( + silent({ + onMatch: 'warn', + customPatterns: [{ id: 'acme', label: 'Acme token', regex: '\\bACME-[A-Z0-9]{6,}' }], + }), + ); + const result = await executeHooks({ + registry: built!.registry, + input: promptInput('starter sk-anything-here and custom ACME-DEADBEEF12'), + }); + + const ids = built!.collector.matches.map((m) => m.patternId).sort(); + expect(ids).toContain('sk_prefix'); + expect(ids).toContain('acme'); + expect(result.updatedPrompt).toContain('sk-[REDACTED]'); + expect(result.updatedPrompt).toContain('[REDACTED]'); }); it('honors a custom redactionText', async () => { diff --git a/packages/api/src/agents/messagePiiFilter.ts b/packages/api/src/agents/messagePiiFilter.ts index 9879c4e3fd..71919038c7 100644 --- a/packages/api/src/agents/messagePiiFilter.ts +++ b/packages/api/src/agents/messagePiiFilter.ts @@ -91,7 +91,9 @@ export function createMessagePiiFilterHooks( return {}; } - collector.matches.push(...matches); + if (mode !== 'silent') { + collector.matches.push(...matches); + } if (mode === 'block') { logger.info( @@ -108,13 +110,11 @@ export function createMessagePiiFilterHooks( // silent + warn both redact server-side. The difference is // that warn surfaces the matches to the UI via the controller // (which reads collector.matches after processStream resolves). - if (mode === 'warn') { - logger.info( - `[messagePiiFilter] redacted ${matches.length} match(es) (mode=warn, patterns=${matches - .map((m) => m.patternId) - .join(',')})`, - ); - } + logger.info( + `[messagePiiFilter] redacted ${matches.length} match(es) (mode=${mode}, patterns=${matches + .map((m) => m.patternId) + .join(',')})`, + ); return { updatedPrompt: text }; },