Skip to content

fix(auth): keep provider-owned Claude OAuth pools intact on login - #1309

Merged
code-yeongyu merged 3 commits into
mainfrom
fix/claude-sdk-oauth-login-pool
Sep 3, 2026
Merged

fix(auth): keep provider-owned Claude OAuth pools intact on login#1309
code-yeongyu merged 3 commits into
mainfrom
fix/claude-sdk-oauth-login-pool

Conversation

@code-yeongyu

@code-yeongyu code-yeongyu commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Summary

Adding a second claude-sdk-oauth account produced a broken login-2 slot holding the managed placeholder claude-sdk-oauth-managed instead of the newly issued OAuth tokens. The provider's own login already returns the complete pooled credential, but the shared appendLoginSlot write step read that result's top-level fields as if they were a flat credential and appended them a second time. This PR makes appendLoginSlot write a provider-owned pool through untouched, and teaches the claude-sdk-oauth availability predicate to accept a concrete OAuth account projected by shared credential rotation.

Observably: a second login now stores exactly one new slot carrying its real tokens, that slot participates in affinity and failover, and the second login no longer fails with Provider is not configured: claude-sdk-oauth. First logins, api_key logins, and the ambient (host Claude CLI) lane behave exactly as before.

Changes

Shared credential pool (packages/ai)

  • appendLoginSlot returns the login result unchanged when that result already carries a populated accounts array — a provider that manages its own pool produces the complete post-login credential, so re-appending its top-level fields is double pooling. Every other branch is byte-identical: an absent or flat current still stores the flat credential as-is, and an unnamed flat credential against a pooled current still becomes the next login-N slot with its own material.

claude-sdk-oauth availability (packages/coding-agent)

  • configuredFor, the single predicate behind both check and resolveAmbient, now counts a stored credential whose top-level OAuth fields are concrete (neither access nor refresh is the sentinel) as one account. Slot-scoped resolution projects one named slot onto the flat credential shape and strips accounts, so such a credential previously counted as zero accounts. A projected sentinel still counts as zero, leaving the ambient opt-in requirement untouched.

Tests and docs

  • The exact claude-sdk-oauth second login stores sentinel as login-2 credential #1279 in-memory repro plus boundary cases in packages/ai/test/credential-pool-mutations.test.ts; projected-slot and projected-sentinel check cases in packages/coding-agent/test/claude-sdk-oauth-login.test.ts.
  • Tracker entries in packages/ai/src/changes.md and .../claude-sdk-oauth/changes.md, plus [Unreleased] > Fixed bullets in both package changelogs.

QA & Evidence

All test, typecheck, and build runs executed on a remote bunshin machine (mengmotaMac, bun 1.4.0, node v26.7.0). Evidence lives under /Users/yeongyu/sisyphuslabs/.omo/evidence/ulw/claude-sdk-fable51-20260903.

Check What was tested Observed result Artifact
RED New tests against untouched 9892d3eb2 sources 2 genuine assertion failures (login-2 holds claude-sdk-oauth-managed; check returns undefined) — no import errors g4-red.log
GREEN Same commands with the fix applied packages/ai 18/18, packages/coding-agent 8/8 g4-green.log
Mutation Each guard reverted independently, then restored Removing the appendLoginSlot early return kills only the #1279 test; removing selectedStoredAccount kills only the projected-slot test; restoring both is green again g4-mutation.log
Edge Verbose run naming every boundary case Unnamed flat OAuth still becomes login-2 with its own tokens; flat api_key still appends; appendLoginSlot(undefined, flat) returns identity; projected sentinel still rejected — 14/14 + 8/8 g4-edge.log
Regression On this branch with --build: tsc --noEmit -p tsconfig.build.json in both packages plus the credential-pool, auth, and claude-sdk-oauth suites (login, accounts, auth-lane, availability, all ambient, extension, account-command) Build ok, both typechecks clean, 73 + 73 tests passed, cmd-exit=0 g4-regression.log
Changelog gate node scripts/check-pr-changelog.mjs --base 9892d3eb2 changelog-gate: PASS g4-lane-report.md

The mutation pass is what makes this sufficient: each of the two production hunks has exactly one test that dies without it, so neither guard is passing by accident, and the edge run proves the untouched branches of appendLoginSlot still behave as before.

Note: neither package ships a plain tsconfig.json; both typechecks use tsconfig.build.json.

Risks & Residuals

  • Any provider whose login returns a populated accounts array now owns its entire pool write. appendLoginSlot's only callers are ModelsImpl.login and AuthStorage.set, and claude-sdk-oauth is the only in-tree provider that returns a pooled login result, so no other provider's behavior changes.
  • A genuinely flat-but-concrete stored OAuth credential (a legacy single-account entry) now counts as one configured account in the claude-sdk-oauth predicate. That is the intended reading, and it matches the referenced PRs.
  • Not addressed here: fix(auth): preserve legacy OAuth credential when adding accounts #1308 (LAB-109 legacy-flat promotion for openai-codex) is a separate concern and stays open.

Related Issues


Summary by cubic

Adding a second claude-sdk-oauth account no longer produces a broken login-2 slot holding the managed placeholder, and that slot no longer fails with Provider is not configured once rotation selects it. The shared pool writer now passes a provider-owned pooled login result through untouched, and the claude-sdk-oauth availability predicate counts a rotation-projected concrete slot as one configured account.

  • appendLoginSlot returns unchanged any login result that already carries a populated accounts array; first logins, flat api_key logins, and unnamed-slot writes keep their existing behavior.
  • The availability predicate counts a flat OAuth credential with concrete (non-sentinel) tokens as one account; a sentinel still counts as zero, so the ambient opt-in path is unchanged.
  • Fixes claude-sdk-oauth second login stores sentinel as login-2 credential #1279.

Written for commit 9034138. Summary will update on new commits.

Review in cubic

code-yeongyu and others added 3 commits September 3, 2026 12:35
A provider whose own login returns the complete pooled credential was double-pooled: appendLoginSlot read that result's top-level fields as a flat credential and appended them as a second slot. For claude-sdk-oauth those fields are the managed sentinel, so a second account produced a login-2 slot holding placeholder tokens.

Adopts the slots.ts hunk from #1304 and #1196 by @eddieparc.

Ultraworked with [omo](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
…d account

Shared credential rotation projects one named slot onto the flat credential shape and strips accounts, so the availability predicate counted zero accounts and reported "Provider is not configured: claude-sdk-oauth". A projected sentinel still counts as zero, leaving the ambient opt-in path unchanged.

Adopts the oauth-login.ts hunk from #1304 and #1196 by @eddieparc.

Ultraworked with [omo](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
Ultraworked with [omo](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
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.

claude-sdk-oauth second login stores sentinel as login-2 credential

1 participant