From 60448d4f3143fbb2b6b3183f43febb372a4fe494 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Thu, 16 Jul 2026 14:13:26 -0300 Subject: [PATCH] fix(executors): forward X-Session-ID/X-Title agent metadata headers (#7104) * fix(executors): forward X-Session-ID/X-Title agent metadata headers (port from 9router#2413) Custom agent clients (e.g. non-OpenCode providers) commonly send X-Session-ID and X-Title headers for upstream request tracking/attribution, but forwardOpencodeClientHeaders() only forwarded x-opencode-* keys plus User-Agent, silently dropping these for every client. Extends the existing case-insensitive allowlist forwarding path with x-session-id/x-title. Reported-by: Atikur Rahman Chitholian (@chitholian) (https://github.com/decolua/9router/issues/2413) * chore(changelog): move #2413 entry to changelog.d fragment Consistency with the repo's canonical changelog.d/fixes/ workflow (avoids merge-storm re-conflicts from editing CHANGELOG.md directly). --- .../fixes/2413-preserve-agent-headers.md | 1 + open-sse/utils/opencodeHeaders.ts | 19 ++++++++++ tests/unit/refactor-opencodeHeaders.test.ts | 37 +++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 changelog.d/fixes/2413-preserve-agent-headers.md diff --git a/changelog.d/fixes/2413-preserve-agent-headers.md b/changelog.d/fixes/2413-preserve-agent-headers.md new file mode 100644 index 0000000000..6353f5dd77 --- /dev/null +++ b/changelog.d/fixes/2413-preserve-agent-headers.md @@ -0,0 +1 @@ +- **fix(executors):** forward agent-supplied `X-Session-ID`/`X-Title` metadata headers to upstream providers — previously dropped for every client outside the `x-opencode-*` allowlist. (thanks @chitholian) (#7104) diff --git a/open-sse/utils/opencodeHeaders.ts b/open-sse/utils/opencodeHeaders.ts index 4e1221877c..8569c8512c 100644 --- a/open-sse/utils/opencodeHeaders.ts +++ b/open-sse/utils/opencodeHeaders.ts @@ -12,6 +12,15 @@ const OPENCODE_HEADER_KEYS = [ "x-opencode-client", ] as const; +/** + * Common agent-metadata headers used by non-OpenCode clients (custom agents/ + * providers) for upstream request tracking and attribution. Forwarded the same + * way as the x-opencode-* set: case-insensitive lookup, client value wins. + * Added for 9router#2413 — these were previously dropped for every client + * outside the OpenCode allowlist. + */ +const AGENT_METADATA_HEADER_KEYS = ["x-session-id", "x-title"] as const; + /** * Case-insensitive lookup for a header in a headers record. */ @@ -26,6 +35,8 @@ function findHeader(headers: Record, name: string): string | und * 1. Forwards User-Agent from clientHeaders via `setUserAgentHeader()` * 2. Forwards x-opencode-session, x-opencode-request, x-opencode-project, * x-opencode-client headers (case-insensitive match) + * 3. Forwards x-session-id, x-title agent-metadata headers (case-insensitive + * match) — common conventions used by non-OpenCode agent clients (9router#2413) * * @param headers - The outbound headers record to mutate * @param clientHeaders - The client-provided headers to forward from @@ -60,6 +71,14 @@ export function forwardOpencodeClientHeaders( } } + // 2b. Forward agent-metadata headers (x-session-id, x-title) — 9router#2413 + for (const headerName of AGENT_METADATA_HEADER_KEYS) { + const value = findHeader(clientHeaders, headerName); + if (value) { + headers[headerName] = value; + } + } + // 3. OpencodeExecutor-only: synthesize session/request id from fallback headers if (options?.synthesizeRequestId && !headers["x-opencode-session"]) { const sessionAffinity = diff --git a/tests/unit/refactor-opencodeHeaders.test.ts b/tests/unit/refactor-opencodeHeaders.test.ts index b79865b30e..19a5a1cdf6 100644 --- a/tests/unit/refactor-opencodeHeaders.test.ts +++ b/tests/unit/refactor-opencodeHeaders.test.ts @@ -116,6 +116,43 @@ describe("forwardOpencodeClientHeaders – x-opencode-* headers", () => { }); }); +// ── agent metadata headers (X-Session-ID / X-Title) — 9router#2413 ───────── +// Non-OpenCode agent clients (e.g. custom providers) commonly send X-Session-ID +// and X-Title for upstream request tracking/attribution. These were previously +// dropped for every client outside the x-opencode-* allowlist. + +describe("forwardOpencodeClientHeaders – X-Session-ID / X-Title", () => { + it("forwards X-Session-ID from client headers", () => { + const headers = h(); + const clientHeaders = { "X-Session-ID": "sess-xyz" }; + forwardOpencodeClientHeaders(headers, clientHeaders); + assert.equal(headers["x-session-id"], "sess-xyz"); + }); + + it("forwards X-Title from client headers", () => { + const headers = h(); + const clientHeaders = { "X-Title": "My Agent" }; + forwardOpencodeClientHeaders(headers, clientHeaders); + assert.equal(headers["x-title"], "My Agent"); + }); + + it("matches X-Session-ID / X-Title case-insensitively", () => { + const headers = h(); + const clientHeaders = { "x-session-id": "sess-lower", "x-title": "lower title" }; + forwardOpencodeClientHeaders(headers, clientHeaders); + assert.equal(headers["x-session-id"], "sess-lower"); + assert.equal(headers["x-title"], "lower title"); + }); + + it("still does NOT forward unrelated unknown headers", () => { + const headers = h(); + const clientHeaders = { "X-Session-ID": "sess-1", "X-Random-Other": "nope" }; + forwardOpencodeClientHeaders(headers, clientHeaders); + assert.equal(headers["x-session-id"], "sess-1"); + assert.equal(headers["X-Random-Other"], undefined); + }); +}); + // ── synthesizeRequestId ───────────────────────────────────────────────────── describe("forwardOpencodeClientHeaders – synthesizeRequestId", () => {