Skip to content

[OAuth] Bind interactive login to explicit Connection entities #3888

Description

@me2seeks

Problem

Interactive OAuth login was bound only to an already-created connectionId. Desktop created or enabled the first provider Connection before authorization, selected the first Connection for a provider, and a new login silently superseded an active login. This could not safely represent multiple openai-codex or xai-oauth accounts as explicit entities.

Child workstream of #3852. Stacked after #3882.

Scope

Deepen the existing OAuth enrollment lifecycle without adding new wire operations:

  • Change oauth.login.start to accept an explicit target:
    • create(providerType)
    • existing(connectionId)
  • Return the canonical Connection identity connectionId + slug + providerType in every login projection.
  • Let Storage allocate the create candidate identity and canonical OAuth slug:
    • openai-codex -> codex-subscription[-N]
    • xai-oauth -> xai-oauth[-N]
  • Keep create attempts invisible to the catalog until authorization succeeds.
  • Commit the OAuth token and Connection through one recoverable Connection-enrollment transaction.
  • Allow an existing disabled OAuth Connection to log in again and re-enable that exact entity without changing its name, slug, models, or default selection.
  • Replace implicit supersede with explicit operation_conflict while another attempt is active.
  • Migrate Desktop to the closed wire: provider-level add uses create(providerType), while Connection detail re-login uses existing(connectionId) and synchronizes the exact returned entity.
  • Keep malformed IDs, provider/Connection mismatches, and start/terminal identity drift fail-closed.
  • Advance the Runtime Host compatibility epoch from 53 to 54.

Durable identity and recovery contract

  • The existing physical runtime-policy-onboarding.json journal keeps v1/v2 compatibility and gains a discriminated v3 oauth_enrollment intent.
  • Commit order is: preflight -> durable intent -> vault -> catalog -> authenticated receipt -> clear intent.
  • Recovery runs before orphan credential cleanup and converges to the exact candidate identity.
  • A bounded, secret-free ledger retains the latest 256 authenticated attempt receipts:
    • exact original target fingerprint
    • final Connection identity
    • authenticated phase and deterministic completion order
  • Within that retention window, start/query/cancel can recover an authenticated outcome after Host restart or an undelivered response. The same attempt and same target are idempotent; the same attempt with another target is invalid.
  • Receipt eviction is explicitly bounded: an evicted attempt queries as not_found and is no longer protected by the old idempotency key.

Acceptance criteria

  • Two create logins for one OAuth provider commit distinct immutable IDs, unique compatible slugs, and credentials at connection:<id>:oauth_token.
  • Failed, cancelled, unavailable-presentation, and provider-rejected create attempts leave no catalog or vault entity.
  • Existing enabled and disabled Connections reauthenticate the exact entity; Connection and credential CAS drift fail closed with distinct diagnostics.
  • Different active attempts conflict and never silently cancel one another; same-attempt retries are idempotent.
  • Journal and receipt failure points recover without duplicate Connections, lost tokens, orphan cleanup, or identity replacement.
  • Lost start response followed by Host restart can be reconciled using only the original attemptId.
  • Closed protocol codecs reject the epoch-53 start/projection shapes; epoch guard covers 54.
  • Existing Desktop single-account OAuth behavior remains functional; exact detail re-login never falls back to another account.

Non-goals

  • Desktop multi-account cards, explicit account picker, status/refresh/logout UX
  • Concurrent per-Connection OAuth login lanes
  • Automatic account selection, balancing, failover, or default-target changes
  • OAuth email address as identity or slug

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions