Skip to content

fix(sse): stop leaking a foreign combo/pool account's Codex quota headers (#14116) - #14465

Merged
diegosouzapw merged 3 commits into
release/v3.8.51from
fix/14116-codex-quota-header-leak
Sep 24, 2026
Merged

diegosouzapw merged 3 commits into
release/v3.8.51from
fix/14116-codex-quota-header-leak

Conversation

@diegosouzapw

Copy link
Copy Markdown
Owner

Root cause

buildStreamingResponseHeaders() (open-sse/handlers/chatCore/responseHeaders.ts) forwarded the
x-codex-*-used-percent/-reset/-window/-credits/-plan-type quota headers unconditionally, with no
notion of which account actually served the request vs which one the caller pinned/requested. When
combo/pool routing served a response through a sibling account, that account's quota leaked to the
caller.

An earlier attempt (#13638, closed unmerged) tried an unconditional strip of these headers, which
would have broken the direct-path regression guard for #10315 (the direct path unambiguously owns
its own quota headers and must keep forwarding them). This PR strips them only on a proven
account mismatch instead.

Fix

  • open-sse/handlers/chatCore/responseHeaders.ts: extracted the existing x-codex-* quota-header
    match into an exported isCodexAccountQuotaHeader() predicate (shared by the forwarding-priority
    boost and the new strip step), and added isForeignComboAccountResponse() — true only when
    isCombo && requestedConnectionId && selectedConnectionId && requestedConnectionId !== selectedConnectionId. buildStreamingResponseHeaders() now drops quota-header candidates when
    that predicate is true, before they enter the forwarding budget.
  • open-sse/handlers/chatCore/streamingResponseHeaders.ts: threads isCombo /
    requestedConnectionId / selectedConnectionId through assembleStreamingResponseHeaders().
  • open-sse/handlers/chatCore.ts: added an optional forcedConnectionId param to handleChatCore,
    passed through to the header-assembly call site alongside isCombo and
    credentials?.connectionId as selectedConnectionId.
  • src/sse/handlers/chatHelpers.ts / src/sse/handlers/chat.ts: thread the caller's
    pinned/requested connection id (forcedConnectionId) down from the existing
    requestedConnectionId/hasForcedConnection plumbing into executeChatWithBreaker() →
    handleChatCore().

The direct path (no combo) and unpinned combo requests (no explicit connection pin) keep today's
forwarding behavior unchanged — the strip only triggers on a proven mismatch between the caller's
own pinned/requested connection and the connection that actually served the response.

Regression test

tests/unit/codex-quota-header-leak-14116.test.ts (new file, 3 cases):

  • RED (pre-fix, verified by reverting the fix hunks and re-running):
    ✖ #14116 (BUG): combo/pool path where the selected account is a FOREIGN account must NOT leak its quota headers
      AssertionError [ERR_ASSERTION]: x-codex-primary-used-percent leaked a foreign combo account's quota to the caller
      '41' !== undefined
    tests 3 / pass 2 / fail 1
    
  • GREEN (with the fix):
    ✔ #14116: direct path (no combo) keeps forwarding Codex quota headers — #10315 must stay intact
    ✔ #14116: combo/pool path where the selected account IS the caller's own keeps forwarding quota headers
    ✔ #14116 (BUG): combo/pool path where the selected account is a FOREIGN account must NOT leak its quota headers
    tests 3 / pass 3 / fail 0
    

Gates run

  • node --import tsx/esm --test tests/unit/codex-quota-header-leak-14116.test.ts → 3/3 pass
  • npm run typecheck:core → exit 0
  • node scripts/check/check-open-sse-typecheck.mjs → exit 0
  • npx eslint --suppressions-location config/quality/eslint-suppressions.json <all 6 changed files> → exit 0
  • node scripts/check/check-file-size.mjs → OK (see Rebaseline section below)
  • node scripts/check/check-complexity-ratchets.mjs --base-ref origin/release/v3.8.51 → OK (new-code violations: complexity 0, cognitive 0, both baseline-neutral on the 5 touched files)
  • node scripts/check/check-changelog-integrity.mjs → not evaluated as a real signal here: this branch was intentionally NOT rebased onto the current release tip (7 commits behind, per instructions), so the local CHANGELOG.md is stale relative to origin/release/v3.8.51 and the script reports 474 bullets "missing" that are simply later, unrelated merges. This PR does not touch CHANGELOG.md (only adds a changelog.d/fixes/ fragment, per convention) — 0 bullets removed by this diff. CI evaluates the actual PR-merge result against the current base, which will be unaffected by this branch's lag.
  • npm run check:public-creds — not run; no public-cred literal lines were moved or added.

Existing tests

Ran every test file referencing the touched symbols/files (buildStreamingResponseHeaders,
assembleStreamingResponseHeaders, isCodexAccountQuotaHeader, responseHeaders.ts,
middleware-header-strip):

  • tests/unit/13601-header-drop-count-surfaced.test.ts — 3/3
  • tests/unit/chatcore-header-drop-warn-dedupe-10315.test.ts — 5/5
  • tests/unit/chatcore-streaming-response-headers.test.ts — 5/5
  • tests/unit/codex-turn-state.test.ts — 9/9
  • tests/unit/middleware-header-strip-5849.test.ts (the fix(backend): deduplicate repeated upstream-header budget warnings #10315 direct-path regression guard) — 9/9, all green, unchanged
  • tests/unit/omniroute-decision-header.test.ts — 9/9
  • tests/unit/chatcore-translation-paths.test.ts — 79/79
  • tests/unit/g13-combo-chatcore-golden.test.ts (public-behavior golden lock on chatCore.ts) — 3/3

None were modified — all passed as-is against the fix.

Rebaseline (needs owner approval)

config/quality/file-size-baseline.json gained one entry,
_rebaseline_2026_09_21_14116_codex_quota_header_leak, raising two frozen caps by +1 line each:
src/sse/handlers/chat.ts 2547→2548 and src/sse/handlers/chatHelpers.ts 1253→1254. I re-audited
this before opening the PR and could not reduce it further: each is one field added to an existing
multi-line call-site object literal (forcedConnectionId: hasForcedConnection ? forcedConnectionId : null in chat.ts, and one destructured param + one passthrough field in chatHelpers.ts,
already offset there by compacting an adjacent 3-line comment to 2 lines). open-sse/handlers/chatCore.ts
stayed within its existing cap (6282 vs 6287) without any adjustment. The new predicate/strip logic
itself (isCodexAccountQuotaHeader, isForeignComboAccountResponse) lives entirely in the
non-frozen open-sse/handlers/chatCore/responseHeaders.ts, so only the two thin plumbing call
sites needed the +1/+1.

Prior art

Credit to @fenix007 for the original report (#13638, closed unmerged — the unconditional-strip
attempt this PR avoids repeating) and to @shubhayu-dev, whose parallel PR #14137 (against #13638,
still open) independently arrived at the same isForeignAccount-style framing.

Closes #14116

Plan-file: _tasks/pipeline/bugs/2-implementing/14116-fix-sse-codex-quota-headers-leak-the-selected-pool-combo-accou.plan.md

…ders (#14116)

buildStreamingResponseHeaders() forwarded the x-codex-*-used-percent/-reset/
-window/-credits/-plan-type quota headers unconditionally, with no notion of
which account actually served the request vs which one the caller pinned or
requested. When combo/pool routing served a response through a sibling
account, that account's quota leaked to the caller.

Thread the caller's pinned/requested connection id (forcedConnectionId) from
src/sse/handlers/chat.ts through chatHelpers.ts and chatCore.ts down to the
header-assembly chokepoint, and compare it against the connection that
actually served the response. When combo routing served a foreign account
(pinned/requested connection differs from the one that served it), strip the
Codex quota headers before they reach the caller. The direct path (no combo)
and unpinned combo requests keep today's forwarding behavior unchanged.

An earlier attempt (#13638) tried an unconditional strip, which would have
broken the direct-path regression guard for #10315 — this fix is conditional
on a proven account mismatch instead.

Refs #14116

@shubhayu-dev shubhayu-dev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merging as approved

# Conflicts:
#	config/quality/file-size-baseline.json
diegosouzapw added a commit that referenced this pull request Sep 24, 2026
… key compared without timing-safe check (#14485) (#14571)

Merge-batch 2026-09-23 (PRs do mantenedor, Trilha B).

A reconciliação com o tip atual de `release/v3.8.51` foi feita no branch da PR; o detalhe está na seção "Rework (merge-batch 2026-09-23)" do corpo, quando existe.
- #14571: a política de API key passa a rodar só quando o divert self-hosted está configurado (antes rodava 2x em toda request cloud). Prova red→green: o teste "cloud request runs the key policy ONCE" dava 429 na versão anterior e agora 5/5 passam; 29/29 no total.
- #14465: re-medido depois do merge do tip. chat.ts 2560→2561 e chatHelpers.ts 1257→1258 (+1 cada, plumbing de `forcedConnectionId`), com anotação datada dentro de `frozen`. 11/11 testes focados (inclui o guard #5849).
- #14467: reconciliada depois da #14468 (suno). Contagem de providers regerada = 358 (`gen:provider-reference` + `check:provider-consistency`); REMOVED_PROVIDERS/blocklist com as duas entradas; tripwire de prefixos reservados re-medido em 412; AGENTS.md/llm.txt mudam só o número (aprovado pelo dono). 57/57 testes focados.
- Em todas: `typecheck:core` mostra só o herdado `cliproxyAccountHealth.ts:157`, e o único vermelho de `check:open-sse-typecheck` é o herdado `auggie.ts` (#14547).
@diegosouzapw
diegosouzapw merged commit ee372a4 into release/v3.8.51 Sep 24, 2026
14 of 21 checks passed
diegosouzapw added a commit that referenced this pull request Sep 24, 2026
…nt to validate rewrite (#14217) (#14467)

Merge-batch 2026-09-23 (PRs do mantenedor, Trilha B).

A reconciliação com o tip atual de `release/v3.8.51` foi feita no branch da PR; o detalhe está na seção "Rework (merge-batch 2026-09-23)" do corpo, quando existe.
- #14571: a política de API key passa a rodar só quando o divert self-hosted está configurado (antes rodava 2x em toda request cloud). Prova red→green: o teste "cloud request runs the key policy ONCE" dava 429 na versão anterior e agora 5/5 passam; 29/29 no total.
- #14465: re-medido depois do merge do tip. chat.ts 2560→2561 e chatHelpers.ts 1257→1258 (+1 cada, plumbing de `forcedConnectionId`), com anotação datada dentro de `frozen`. 11/11 testes focados (inclui o guard #5849).
- #14467: reconciliada depois da #14468 (suno). Contagem de providers regerada = 358 (`gen:provider-reference` + `check:provider-consistency`); REMOVED_PROVIDERS/blocklist com as duas entradas; tripwire de prefixos reservados re-medido em 412; AGENTS.md/llm.txt mudam só o número (aprovado pelo dono). 57/57 testes focados.
- Em todas: `typecheck:core` mostra só o herdado `cliproxyAccountHealth.ts:157`, e o único vermelho de `check:open-sse-typecheck` é o herdado `auggie.ts` (#14547).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(sse): Codex quota headers leak the selected pool/combo account's quota to the caller

2 participants