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
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 multipleopenai-codexorxai-oauthaccounts as explicit entities.Child workstream of #3852. Stacked after #3882.
Scope
Deepen the existing OAuth enrollment lifecycle without adding new wire operations:
oauth.login.startto accept an explicit target:create(providerType)existing(connectionId)connectionId + slug + providerTypein every login projection.openai-codex -> codex-subscription[-N]xai-oauth -> xai-oauth[-N]operation_conflictwhile another attempt is active.create(providerType), while Connection detail re-login usesexisting(connectionId)and synchronizes the exact returned entity.Durable identity and recovery contract
runtime-policy-onboarding.jsonjournal keeps v1/v2 compatibility and gains a discriminated v3oauth_enrollmentintent.start/query/cancelcan 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.not_foundand is no longer protected by the old idempotency key.Acceptance criteria
connection:<id>:oauth_token.attemptId.Non-goals