From df3a6cf674560a4af4f4c1cef5e2d3123039ff6e Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Sun, 12 Jul 2026 02:00:19 -0300 Subject: [PATCH] fix(providers): route AgentRouter key validation through CC wire image (#6377) (#6882) * fix(providers): route AgentRouter key validation through CC wire image (#6377) * test(6377): type fetch mock to satisfy no-explicit-any gate --- .../fixes/6377-6377-agentrouter-key.md | 1 + src/lib/providers/validation.ts | 26 ++++++ .../repro-6377-agentrouter-validation.test.ts | 81 +++++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 changelog.d/fixes/6377-6377-agentrouter-key.md create mode 100644 tests/unit/repro-6377-agentrouter-validation.test.ts diff --git a/changelog.d/fixes/6377-6377-agentrouter-key.md b/changelog.d/fixes/6377-6377-agentrouter-key.md new file mode 100644 index 0000000000..c9bc47e827 --- /dev/null +++ b/changelog.d/fixes/6377-6377-agentrouter-key.md @@ -0,0 +1 @@ +- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377) diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index 08eccca4d6..01a13c7339 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -17,6 +17,11 @@ import { MODAL_DEFAULT_VALIDATION_MODEL_ID } from "@/shared/constants/modal"; import { validateQoderCliPat } from "@omniroute/open-sse/services/qoderCli.ts"; import { validateImageProviderApiKey } from "@/lib/providers/imageValidation"; import { KiroService } from "@/lib/oauth/services/kiro"; +import { usesCcWireImage } from "@omniroute/open-sse/services/ccWireImageBuiltins.ts"; +import { + buildProviderHeaders, + buildProviderUrl, +} from "@omniroute/open-sse/services/provider.ts"; import { OPENAI_LIKE_FORMATS, @@ -825,6 +830,27 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi } if (entry.format === "claude") { + // Built-in CC-wire-image providers (e.g. agentrouter, #6056/#6255) gate + // their WAF on the dynamic Claude-Code fingerprint (User-Agent, + // `?beta=true` chat path, anthropic-beta/x-app/X-Stainless-* headers). + // The real chat-request path already routes through + // buildProviderUrl/buildProviderHeaders for this; the validation probe + // must use the SAME wire image or a genuinely valid key gets 403'd as + // "unauthorized client detected" (#6377). + if (usesCcWireImage(provider)) { + const requestBaseUrl = buildProviderUrl(provider, modelId, true, { baseUrl }); + const requestHeaders = buildProviderHeaders(provider, { apiKey }, true); + + return await validateAnthropicLikeProvider({ + apiKey, + baseUrl: requestBaseUrl, + modelId, + headers: requestHeaders, + providerSpecificData, + isLocal, + }); + } + const requestBaseUrl = `${baseUrl}${entry.urlSuffix || ""}`; const requestHeaders = { ...(entry.headers || {}), diff --git a/tests/unit/repro-6377-agentrouter-validation.test.ts b/tests/unit/repro-6377-agentrouter-validation.test.ts new file mode 100644 index 0000000000..cebc459990 --- /dev/null +++ b/tests/unit/repro-6377-agentrouter-validation.test.ts @@ -0,0 +1,81 @@ +// Repro for GitHub issue #6377: AgentRouter "Check" (validate API key) returns +// "Invalid API key" for a genuinely valid token. +// +// Root cause: PR #6255 (#6056) routed the REAL chat-request path for the +// built-in `agentrouter` provider through the dynamic Claude-Code wire image +// (buildProviderHeaders/buildProviderUrl -> CC fingerprint headers + the +// `?beta=true` chat path) specifically because AgentRouter's WAF rejects +// requests that don't look like the official Claude Code client +// ("unauthorized client detected" — see the comment in +// open-sse/config/providers/registry/agentrouter/index.ts). +// +// validation.ts's generic `entry.format === "claude"` branch (used for the +// dashboard "Check" button) was NOT updated by that PR: it still builds a +// bare request (Content-Type + x-api-key + anthropic-version only, no +// `?beta=true`, no CC fingerprint headers) straight to entry.baseUrl. A WAF +// that gates on the CC wire image will legitimately 403 this validation +// probe even though the same key works for real chat traffic — the exact +// mismatch the reporter describes. +// +// This test simulates that WAF: accept only requests that carry the CC wire +// image markers (User-Agent: claude-cli/... and the `?beta=true` chat path); +// reject everything else with 403 "unauthorized client detected", mapped by +// the validator to { valid: false, error: "Invalid API key" }. + +import test from "node:test"; +import assert from "node:assert/strict"; + +const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts"); + +const originalFetch = globalThis.fetch; + +test.afterEach(() => { + globalThis.fetch = originalFetch; +}); + +test("agentrouter key validation must not false-negative behind the CC-wire-image WAF gate (#6377)", async () => { + const calls: { url: string; headers: Record }[] = []; + + globalThis.fetch = async (url: string | URL, init: RequestInit = {}) => { + const u = String(url); + const headers: Record = {}; + if (init?.headers) { + for (const [k, v] of Object.entries(init.headers)) { + headers[k.toLowerCase()] = String(v); + } + } + calls.push({ url: u, headers }); + + // Emulate AgentRouter's real-world WAF: only requests that look like the + // official Claude Code client (CC wire image: `?beta=true` chat path + + // a claude-cli User-Agent) are let through with a valid key. Anything + // else — even with the SAME valid key — is rejected as an + // "unauthorized client". + const looksLikeClaudeCode = + u.includes("beta=true") && /claude-cli/i.test(headers["user-agent"] || ""); + + if (!looksLikeClaudeCode) { + return new Response(JSON.stringify({ error: "unauthorized client detected" }), { + status: 403, + }); + } + + return new Response(JSON.stringify({ id: "msg_ok" }), { status: 200 }); + }; + + const result = await validateProviderApiKey({ + provider: "agentrouter", + apiKey: "sk-genuinely-valid-agentrouter-key", + providerSpecificData: {}, + }); + + // BEFORE the fix: validation.ts's generic `entry.format === "claude"` + // branch sends a bare request (no `?beta=true`, no CC User-Agent) and gets + // 403'd by the WAF -> false "Invalid API key" for a key that actually works. + assert.equal( + result.valid, + true, + `expected the valid key to validate, got: ${JSON.stringify(result)} — ` + + `requests made: ${JSON.stringify(calls.map((c) => ({ url: c.url, ua: c.headers["user-agent"] })))}` + ); +});