Neutralize accidental @everyone/@here in outbound agent text
Agent-authored replies pass through sanitizeForOutbound before Discord send, but it did not touch mass-mention tokens — so a reply that merely *mentioned* "@everyone" while explaining a feature pinged the whole channel. Add neutralizeMassMentions (zero-width space after @) to the central sanitizer so normal reply/progress/edit text can never mass-ping; intentional broadcasts (e.g. the disk-usage alert) use a dedicated path. Leaves <@id> mentions and emails intact. Covered by unit tests. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
45
src/router-mass-mentions.test.ts
Normal file
45
src/router-mass-mentions.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { neutralizeMassMentions, sanitizeForOutbound } from './router.js';
|
||||
|
||||
const ZWSP = '\u200b';
|
||||
|
||||
describe('neutralizeMassMentions', () => {
|
||||
it('neutralizes @everyone / @here so they no longer ping', () => {
|
||||
expect(neutralizeMassMentions('@everyone hi')).toBe(`@${ZWSP}everyone hi`);
|
||||
expect(neutralizeMassMentions('ping @here now')).toBe(
|
||||
`ping @${ZWSP}here now`,
|
||||
);
|
||||
// The rendered text still reads the same to a human...
|
||||
expect(neutralizeMassMentions('@everyone').replace(ZWSP, '')).toBe(
|
||||
'@everyone',
|
||||
);
|
||||
// ...but the literal mention token is broken.
|
||||
expect(neutralizeMassMentions('@everyone')).not.toBe('@everyone');
|
||||
});
|
||||
|
||||
it('leaves user/role mentions, emails, and plain text untouched', () => {
|
||||
expect(neutralizeMassMentions('<@216851709744513024> hello')).toBe(
|
||||
'<@216851709744513024> hello',
|
||||
);
|
||||
expect(neutralizeMassMentions('mail me@example.com')).toBe(
|
||||
'mail me@example.com',
|
||||
);
|
||||
expect(neutralizeMassMentions('everyone here knows')).toBe(
|
||||
'everyone here knows',
|
||||
);
|
||||
});
|
||||
|
||||
it('is idempotent (already-neutralized text is unchanged)', () => {
|
||||
const once = neutralizeMassMentions('@everyone and @here');
|
||||
expect(neutralizeMassMentions(once)).toBe(once);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeForOutbound applies mass-mention neutralization', () => {
|
||||
it('breaks an accidental @everyone in normal agent output', () => {
|
||||
const out = sanitizeForOutbound('알림: @everyone 디스크 부족');
|
||||
expect(out).toContain(`@${ZWSP}everyone`);
|
||||
expect(out).not.toContain('@everyone');
|
||||
});
|
||||
});
|
||||
@@ -146,10 +146,24 @@ export function neutralizeStrayMarkdown(text: string): string {
|
||||
/** @deprecated Kept for back-compat; use neutralizeStrayMarkdown. */
|
||||
export const neutralizeStrayBackticks = neutralizeStrayMarkdown;
|
||||
|
||||
/**
|
||||
* Prevent accidental mass pings. Agent-authored text that literally contains
|
||||
* `@everyone` / `@here` must never notify the whole channel — that is only ever
|
||||
* intended via a dedicated broadcast path (e.g. the disk-usage alert), not via a
|
||||
* normal reply/progress/edit message. A zero-width space (U+200B) after the `@`
|
||||
* keeps the text readable ("@everyone" still reads the same) while stopping
|
||||
* Discord from parsing it as a mention. Idempotent, and leaves `<@id>` user/role
|
||||
* mentions and ordinary emails (`me@example.com`) untouched.
|
||||
*/
|
||||
export function neutralizeMassMentions(text: string): string {
|
||||
return text.replace(/@(everyone|here)\b/g, '@\u200b$1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize raw agent output for internal use (storage, IPC, intermediate
|
||||
* channel buffers). Strips internal tags + tool-call leaks and redacts
|
||||
* secrets, but does NOT touch markdown delimiters.
|
||||
* channel buffers). Strips internal tags + tool-call leaks, redacts secrets,
|
||||
* and neutralizes accidental @everyone/@here pings, but does NOT touch markdown
|
||||
* delimiters.
|
||||
*
|
||||
* Use this when the text will pass through another `formatOutbound` call
|
||||
* downstream (e.g., the Discord channel boundary). Applying the markdown
|
||||
@@ -161,7 +175,8 @@ export function sanitizeForOutbound(rawText: string): string {
|
||||
if (!text) return '';
|
||||
text = stripToolCallLeaks(text);
|
||||
if (!text) return '';
|
||||
return redactSecrets(text);
|
||||
text = redactSecrets(text);
|
||||
return neutralizeMassMentions(text);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user