Skip to content

fix(oauth): rebind continuation replay scope after generic OAuth 429 failover - #4926

Merged
lidge-jun merged 1 commit into
lidge-jun:devfrom
luvs01:agent/oauth-failover-rebind-20260917
Sep 18, 2026
Merged

lidge-jun merged 1 commit into
lidge-jun:devfrom
luvs01:agent/oauth-failover-rebind-20260917

Conversation

@luvs01

@luvs01 luvs01 commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • The continuation loop's generic OAuth 429 arm swaps the credential through applyFailoverSnapshot but never rebinds the reasoning replay scope. Both the terminal-guard clone (nextParsed) and the outer request (parsed) keep the failed account's replay identity, so the replayed continuation can disclose or cache reasoning under the previous account's scope (cross-account reasoning disclosure / cache contamination).
  • Rebind both owners with bindRouteReasoningReplayScope right after the new activeAdapter resolves, passing oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot. The key-pool arm directly above already rebinds both owners; the snapshot argument matches what the dispatch-path OAuth arms pass.
  • applyFailoverSnapshot updates transportState.replayOAuthCredentialSnapshot to the admitted account before this point, so the rebind binds the replacement identity.
  • Widen the transportState pick list by the single field the rebind reads, and add a source-level regression unit asserting both rebinds in the continuation arm.

Verification

  • Dev base: 2f025814f3b85a027e8c76f913b1e10f342aba5e (origin/dev at branch time); head 655bdc03c54fa7ee26660aa23a106ae6416e632e / tree 2dd80c19b0d0ed849bc59c5b35868d56d93ad3f8.
  • bun x tsc --noEmit - clean.
  • bun run structure:check, bun run privacy:scan, bun scripts/file-size-ratchet.ts - all pass.
  • bun test tests/oauth/generic-oauth-failover.test.ts - 44 pass / 0 fail, including the new regression unit.
  • bun test tests/adapters/terminal-continuation-owner-rotation.test.ts - 1 pass / 0 fail.
  • Fork CI: diagnostic run 35261395545 (contributor fork Actions) - every lane that ran to completion is green except windows 8/9, which failed in tests/cli/cli-ready-subprocess.test.ts (ready --wait exits immediately on terminal failed readiness: expected exit 1, got 0 after the 10s bound) - a CLI readiness subprocess test outside this PR's changed surface, matching the scattered per-shard flake pattern seen on other heads. macos control hit the known 30-minute dispatch cap tracked in [Bug]: macOS control is cancelled near its 30-minute limit in full dispatch CI #4905, and the remaining queued lanes (test 1/4, 2/4, 4/4, keyring macos, npm-global macos, docker smoke, macos 1/2) were cancelled with it.

Remaining gates

  • Upstream CI on this head.
  • Reviewer note (out of scope, flagging for awareness): the streaming loop's generic OAuth arm in adapter-dispatch.ts applies the failover snapshot through the same helper without a replay-scope rebind on parsed. Kept out of this PR to stay minimal; happy to follow up if wanted.

Review readiness checklist

Summary by CodeRabbit

  • Bug Fixes
    • Improved OAuth failover handling after rate-limit responses.
    • Ensured active credential bindings remain synchronized when requests are retried or routed through a replacement OAuth account.
    • Prevented replayed requests from using stale OAuth credentials during continuation flows.

@coderabbitai

coderabbitai Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 040a6adc-71a4-430d-ae74-8fc807d714e2

📥 Commits

Reviewing files that changed from the base of the PR and between 43cd1ad and 655bdc0.

📒 Files selected for processing (2)
  • src/server/responses/adapter-continuation.ts
  • tests/oauth/generic-oauth-failover.test.ts
 ___________________________________________________________________________
< Edge cases called - they'd like to be included in your 'done' definition. >
 ---------------------------------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the bug Something isn't working label Sep 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Deterministic PR hygiene checks passed.

@lidge-jun

Copy link
Copy Markdown
Owner

리뷰 · 우선순위 76 / 80

이 PR은 continuation 루프의 generic OAuth 429 failover 경로에서 credential만 바꾸고 reasoning replay 소유자를 안 바꾸던 구멍을 막습니다. applyFailoverSnapshot으로 계정은 돌아가는데, terminal-guard 클론(nextParsed)과 바깥 요청(parsed)의 replay identity는 실패한 계정에 묶인 채로 남았습니다. 그래서 다음 continuation이 이전 계정 스코프로 reasoning을 캐시하거나 공개할 수 있었습니다.

현재 dev(HEAD 43cd1ade1, 패키지 2.59.0)의 src/server/responses/adapter-continuation.ts를 보면, 바로 위 key-pool rotation 팔은 이미 bindRouteReasoningReplayScope를 nextParsed와 parsed 둘 다에 호출합니다. generic OAuth 팔만 그 두 바인딩이 빠져 있었습니다. 이번 변경은 그 팔을 key-pool과 같은 모양으로 맞추고, deps 타입에 replayOAuthCredentialSnapshot을 명시해 스냅샷을 넘깁니다. 범위가 작고 보안/계정 경계에 직접 닿아서 가치가 큽니다.

테스트는 tests/oauth/generic-oauth-failover.test.ts에 “terminal continuation rotation rebinds both OAuth replay owners”를 추가해, 소스에서 해당 팔이 applyFailoverSnapshot 다음에 bindRouteReasoningReplayScope를 두 번(nextParsed + parsed) 호출하고 둘 다 transportState.replayOAuthCredentialSnapshot을 쓰는지 고정합니다. 동작 오라클이 아니라 소스 모양 핀이라 #4846 취지와는 결이 다르지만, 같은 파일의 key-pool 회귀를 막는 기존 패턴과 맞춰 둔 선택으로 읽힙니다.

라인 src/server/responses/adapter-continuation.ts (generic OAuth continuation arm) - bindRouteReasoningReplayScope를 nextParsed와 parsed에 연속으로 호출합니다. key-pool 팔과 대칭이라 맞고, Response persistence가 outer parsed를 닫는다는 주석도 이유를 분명히 합니다. 다만 두 호출이 같은 인자 블록을 복붙하고 있어, 나중에 한쪽만 고치면 다시 어긋날 수 있습니다. 지금은 범위상 복붙이 더 안전합니다.

경로 tests/oauth/generic-oauth-failover.test.ts - 소스 슬라이스 길이·문자열 매칭으로 팔을 고정합니다. refactor로 주석/공백만 바뀌어도 깨질 수 있는 테스트지만, “이 팔에 바인딩이 빠지면 안 된다”는 회귀를 값싸게 잡는 목적에는 맞습니다. 가능하면 나중에 실제 429→continuation 후 replay scope 소유자가 새 계정인지 확인하는 동작 테스트가 있으면 더 단단합니다.

경로 createAdapterContinuations deps - replayOAuthCredentialSnapshot을 Pick에 넣은 것은 타입으로 누락을 막는 좋은 한 줄입니다. 호출부가 이미 그 필드를 넘기는지(또는 stub 테스트가 깨지는지) CI에서만 확인하면 됩니다.

메인테이너의 판단이 필요한 지점

  • 소스 모양 핀을 이대로 둘지, 후속으로 동작 오라클을 추가할지
  • CodeRabbit이 draft로 스킵한 상태인지(체크 메시지에 draft 언급) — draft면 ready 후 CI를 다시 볼지
  • exact-head Cross-platform CI가 아직 resolve-pr pending이면 merge를 기다릴지

너의 추천
보안 경계 수정이라 우선 merge 후보입니다. exact-head CI가 녹색이면 merge 하세요. types.ts/config.ts 분할과 무관하고, 중복 PR로 보이지 않습니다. 가능하면 draft/ready 상태만 한 번 확인하세요.

이 댓글은 grok-bot이 작성했습니다

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

✅ READY

  • all PR quality gates passed.

Hygiene

✅ Deterministic PR hygiene checks passed.

@lidge-jun
lidge-jun marked this pull request as ready for review September 18, 2026 09:25
@lidge-jun
lidge-jun merged commit 92cd264 into lidge-jun:dev Sep 18, 2026
33 checks passed
@luvs01
luvs01 deleted the agent/oauth-failover-rebind-20260917 branch September 20, 2026 06:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants