From 493708f575e43a8cf11585f6cdc5c7b405f08405 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Fri, 24 Jul 2026 11:44:37 -0300 Subject: [PATCH] fix(sse): strip third-party-agent signals from the Hermes system prompt that trigger Anthropic 400 extra-usage (#8350) (#8358) --- .../fixes/8350-hermes-oauth-usage-400.md | 1 + open-sse/services/systemTransforms.ts | 26 ++++++- .../settings/components/RoutingTab.tsx | 6 +- .../systemTransformsHermesDefaults.ts | 13 ++++ .../unit/8350-hermes-oauth-usage-400.test.ts | 70 +++++++++++++++++++ tests/unit/system-transforms.test.ts | 6 +- 6 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 changelog.d/fixes/8350-hermes-oauth-usage-400.md create mode 100644 src/app/(dashboard)/dashboard/settings/components/systemTransformsHermesDefaults.ts create mode 100644 tests/unit/8350-hermes-oauth-usage-400.test.ts diff --git a/changelog.d/fixes/8350-hermes-oauth-usage-400.md b/changelog.d/fixes/8350-hermes-oauth-usage-400.md new file mode 100644 index 0000000000..9dde1b4a4c --- /dev/null +++ b/changelog.d/fixes/8350-hermes-oauth-usage-400.md @@ -0,0 +1 @@ +- fix(sse): strip third-party-agent signals from the Hermes system prompt that trigger Anthropic 400 extra-usage (#8350) diff --git a/open-sse/services/systemTransforms.ts b/open-sse/services/systemTransforms.ts index 09eb210887..813bd5350f 100644 --- a/open-sse/services/systemTransforms.ts +++ b/open-sse/services/systemTransforms.ts @@ -96,6 +96,9 @@ export const DEFAULT_OBFUSCATE_WORDS = [ // Open WebUI additions "openwebui", "open-webui", + // Hermes additions (#8350) + "hermes-agent", + "hermes", ]; /** @@ -118,6 +121,19 @@ export const PI_PARAGRAPH_ANCHORS = [ "Pi documentation (read only when the user asks about pi itself", ]; +/** + * Hermes (NousResearch/hermes-agent) paragraph anchors — the doc-link + * paragraph injected by `agent/prompt_builder.py` on the upstream CLI. + * Added on top of the other anchor lists for #8350. + */ +export const HERMES_PARAGRAPH_ANCHORS = [ + "hermes-agent.nousresearch.com", + "github.com/NousResearch/hermes-agent", +]; + +/** Hermes identity paragraph prefixes ("You are Hermes Agent, ..."). */ +export const HERMES_IDENTITY_PREFIXES = ["You are Hermes Agent"]; + // ──────────────────────────────────────────────────────────────────────────── // Per-provider defaults. // ──────────────────────────────────────────────────────────────────────────── @@ -164,12 +180,18 @@ export const DEFAULT_CLAUDE_PIPELINE: TransformOp[] = [ ...DEFAULT_PARAGRAPH_REMOVAL_ANCHORS, ...OPENWEBUI_PARAGRAPH_ANCHORS, ...PI_PARAGRAPH_ANCHORS, + ...HERMES_PARAGRAPH_ANCHORS, ], }, - // Drop "You are OpenCode" + "You are Open WebUI" identity paragraphs. + // Drop "You are OpenCode" + "You are Open WebUI" + "You are Hermes Agent" + // identity paragraphs. { kind: "drop_paragraph_if_starts_with", - prefixes: [...DEFAULT_IDENTITY_PREFIXES, ...OPENWEBUI_IDENTITY_PREFIXES], + prefixes: [ + ...DEFAULT_IDENTITY_PREFIXES, + ...OPENWEBUI_IDENTITY_PREFIXES, + ...HERMES_IDENTITY_PREFIXES, + ], }, // Replace the "Here is some useful information about the environment you are // running in:" billing-gate trigger phrase + the "if OpenCode honestly" diff --git a/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx b/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx index 8694dbcc4f..de394df2f4 100644 --- a/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx @@ -12,6 +12,7 @@ import { } from "@/shared/constants/cliCompatProviders"; import { AI_PROVIDERS } from "@/shared/constants/providers"; import { compareTr } from "@/shared/utils/turkishText"; +import { HERMES } from "./systemTransformsHermesDefaults"; // Provider keys (mirror of open-sse/services/systemTransforms.ts). const PROVIDER_CLAUDE = "claude"; @@ -80,6 +81,8 @@ const DEFAULT_OBFUSCATE_WORDS = [ "codecompanion", "openwebui", "open-webui", + "hermes-agent", + "hermes", ]; // Mirror of DEFAULT_SYSTEM_TRANSFORMS_CONFIG from open-sse/services/systemTransforms.ts. @@ -96,11 +99,12 @@ const DEFAULT_SYSTEM_TRANSFORMS_CLIENT = { ...DEFAULT_PARAGRAPH_REMOVAL_ANCHORS, ...OPENWEBUI_PARAGRAPH_ANCHORS, ...PI_PARAGRAPH_ANCHORS, + ...HERMES.anchors, ], }, { kind: "drop_paragraph_if_starts_with", - prefixes: [...DEFAULT_IDENTITY_PREFIXES, "You are Open WebUI"], + prefixes: [...DEFAULT_IDENTITY_PREFIXES, "You are Open WebUI", ...HERMES.prefixes], }, ...DEFAULT_TEXT_REPLACEMENTS.map((r) => ({ kind: "replace_text" as const, diff --git a/src/app/(dashboard)/dashboard/settings/components/systemTransformsHermesDefaults.ts b/src/app/(dashboard)/dashboard/settings/components/systemTransformsHermesDefaults.ts new file mode 100644 index 0000000000..305726af18 --- /dev/null +++ b/src/app/(dashboard)/dashboard/settings/components/systemTransformsHermesDefaults.ts @@ -0,0 +1,13 @@ +/** + * Hermes (NousResearch/hermes-agent) system-transform anchors — client-side + * mirror constant shared by RoutingTab.tsx and tests/unit/system-transforms.test.ts's + * UI-parity snapshot. Extracted to a standalone module to keep RoutingTab.tsx + * under its frozen file-size baseline (#8350). + * + * Server source of truth: open-sse/services/systemTransforms.ts + * (HERMES_PARAGRAPH_ANCHORS / HERMES_IDENTITY_PREFIXES). + */ +export const HERMES = { + anchors: ["hermes-agent.nousresearch.com", "github.com/NousResearch/hermes-agent"], + prefixes: ["You are Hermes Agent"], +}; diff --git a/tests/unit/8350-hermes-oauth-usage-400.test.ts b/tests/unit/8350-hermes-oauth-usage-400.test.ts new file mode 100644 index 0000000000..fd502183c4 --- /dev/null +++ b/tests/unit/8350-hermes-oauth-usage-400.test.ts @@ -0,0 +1,70 @@ +// Regression test for #8350 — Hermes (NousResearch/hermes-agent) system-prompt +// signals reach Anthropic's native Claude OAuth path untouched, tripping +// `[400] Third-party apps now draw from extra usage, not plan limits.` +// +// Mirrors the "OpenWebUI fixture" / "Pi documentation" style tests in +// tests/unit/system-transforms.test.ts. Fixture text matches the real +// identity + doc-link paragraphs injected by +// NousResearch/hermes-agent/agent/prompt_builder.py (verified via WebFetch +// against the upstream repo during triage). +import test from "node:test"; +import assert from "node:assert/strict"; + +const { applySystemTransformPipeline, DEFAULT_SYSTEM_TRANSFORMS_CONFIG, PROVIDER_CLAUDE } = + await import("../../open-sse/services/systemTransforms.ts"); + +test("Hermes fixture: claude provider drops Hermes identity + doc-link paragraphs (#8350)", () => { + const body = { + system: [ + { + type: "text", + text: [ + "You are Hermes Agent, an intelligent AI assistant created by Nous Research.", + "Guidelines:\n- Be concise.", + "For more information, see the documentation at https://hermes-agent.nousresearch.com/docs.", + ].join("\n\n"), + }, + ], + messages: [{ role: "user", content: "hi" }], + }; + const result = applySystemTransformPipeline( + PROVIDER_CLAUDE, + body, + DEFAULT_SYSTEM_TRANSFORMS_CONFIG + ); + const out = (body.system as Array<{ text: string }>)[0].text; + + assert.ok( + !out.includes("You are Hermes Agent"), + "Hermes identity paragraph should be dropped by the default claude pipeline" + ); + assert.ok( + !out.includes("hermes-agent.nousresearch.com"), + "Hermes doc-link paragraph should be dropped by the default claude pipeline" + ); + // Unrelated legitimate content survives untouched. + assert.ok(out.includes("Guidelines:")); + assert.ok(out.includes("Be concise.")); + assert.ok(result.appliedOpKinds.includes("drop_paragraph_if_contains")); + // No billing header injected on the native OAuth path (native code handles that). + assert.ok(!result.appliedOpKinds.includes("inject_billing_header")); +}); + +test("non-Hermes system prompt passes through byte-identical through the claude pipeline (no drive-by regression)", () => { + 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: "hi" }], + }; + const before = JSON.stringify(body); + applySystemTransformPipeline(PROVIDER_CLAUDE, body, DEFAULT_SYSTEM_TRANSFORMS_CONFIG); + assert.equal( + JSON.stringify(body), + before, + "a normal operator system prompt with no third-party-agent anchors must pass through untouched" + ); +}); diff --git a/tests/unit/system-transforms.test.ts b/tests/unit/system-transforms.test.ts index 208345f853..8c576b4e4b 100644 --- a/tests/unit/system-transforms.test.ts +++ b/tests/unit/system-transforms.test.ts @@ -446,11 +446,13 @@ const UI_DEFAULTS_SNAPSHOT = { "@earendil-works/pi-coding-agent", "/.pi/", "Pi documentation (read only when the user asks about pi itself", + "hermes-agent.nousresearch.com", + "github.com/NousResearch/hermes-agent", ], }, { kind: "drop_paragraph_if_starts_with", - prefixes: ["You are OpenCode", "You are Open WebUI"], + prefixes: ["You are OpenCode", "You are Open WebUI", "You are Hermes Agent"], }, { kind: "replace_text", @@ -481,6 +483,8 @@ const UI_DEFAULTS_SNAPSHOT = { "codecompanion", "openwebui", "open-webui", + "hermes-agent", + "hermes", ], targets: ["system", "messages", "tools"], },