diff --git a/changelog.d/fixes/10484-hermes-obfuscate-zwj.md b/changelog.d/fixes/10484-hermes-obfuscate-zwj.md new file mode 100644 index 0000000000..5e1dc60de1 --- /dev/null +++ b/changelog.d/fixes/10484-hermes-obfuscate-zwj.md @@ -0,0 +1 @@ +- fix(sse): stop ZWJ-obfuscating the substring "hermes" in user messages and hostnames (#10484) diff --git a/open-sse/services/systemTransforms.ts b/open-sse/services/systemTransforms.ts index e7542619e3..2bed25d741 100644 --- a/open-sse/services/systemTransforms.ts +++ b/open-sse/services/systemTransforms.ts @@ -96,9 +96,10 @@ export const DEFAULT_OBFUSCATE_WORDS = [ // Open WebUI additions "openwebui", "open-webui", - // Hermes additions (#8350) - "hermes-agent", - "hermes", + // Do not add "hermes" / "hermes-agent" here. #8350 is handled by + // HERMES_PARAGRAPH_ANCHORS + HERMES_IDENTITY_PREFIXES (system-prompt + // drops only). ZWJ on the short substring "hermes" rewrites user + // messages and hostnames (#10484). ]; /** diff --git a/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx b/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx index 22462ce866..5c844f80cb 100644 --- a/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx @@ -81,8 +81,6 @@ const DEFAULT_OBFUSCATE_WORDS = [ "codecompanion", "openwebui", "open-webui", - "hermes-agent", - "hermes", ]; // Mirror of DEFAULT_SYSTEM_TRANSFORMS_CONFIG from open-sse/services/systemTransforms.ts. diff --git a/tests/unit/8350-hermes-oauth-usage-400.test.ts b/tests/unit/8350-hermes-oauth-usage-400.test.ts index fd502183c4..51288c23be 100644 --- a/tests/unit/8350-hermes-oauth-usage-400.test.ts +++ b/tests/unit/8350-hermes-oauth-usage-400.test.ts @@ -68,3 +68,34 @@ test("non-Hermes system prompt passes through byte-identical through the claude "a normal operator system prompt with no third-party-agent anchors must pass through untouched" ); }); + +// #10484 — #8358 added "hermes" to DEFAULT_OBFUSCATE_WORDS. The ZWJ op +// targets user messages with a case-insensitive, no-word-boundary regex, so +// hostnames and ordinary mentions of the OmniRoute hermes CLI tool were +// rewritten. System-prompt identity drops (#8350) must stay; user text must not +// be mutated. +test("user message containing hermes hostname stays byte-identical (#10484)", () => { + const body = { + system: [ + { + type: "text", + text: "You are a helpful operator-configured assistant. Follow company policy X and always answer in English.", + }, + ], + messages: [ + { + role: "user", + content: "1. hermes\n2. hermes.example.ts.net\n3. Hermes on agent-001\n4. hermeS", + }, + ], + }; + const before = JSON.stringify(body); + applySystemTransformPipeline(PROVIDER_CLAUDE, body, DEFAULT_SYSTEM_TRANSFORMS_CONFIG); + assert.equal( + JSON.stringify(body), + before, + "user text containing the substring hermes must not receive ZWJ obfuscation" + ); + const content = (body.messages[0] as { content: string }).content; + assert.equal(content.includes("\u200d"), false, "no zero-width joiner in user text"); +}); diff --git a/tests/unit/system-transforms.test.ts b/tests/unit/system-transforms.test.ts index cf1f7391e0..047063d663 100644 --- a/tests/unit/system-transforms.test.ts +++ b/tests/unit/system-transforms.test.ts @@ -502,8 +502,6 @@ const UI_DEFAULTS_SNAPSHOT = { "codecompanion", "openwebui", "open-webui", - "hermes-agent", - "hermes", ], targets: ["system", "messages", "tools"], },