# Codex App-Server Account Feature - Detailed Implementation Plan **Date**: 2026-04-20 **Status**: Reference-quality implementation plan **Primary repo**: `claude_team` **Secondary repo**: `agent_teams_orchestrator` only for later parity, not on the first critical path **Canonical architecture reference**: [FEATURE_ARCHITECTURE_STANDARD.md](../FEATURE_ARCHITECTURE_STANDARD.md) ## Executive Summary We should restore the old strong Codex subscription UX, but we should **not** bring back the old legacy Codex transport or legacy OAuth semantics. The correct design is: - keep **execution** on the current `codex-native` runtime path - introduce a new dedicated feature slice: `src/features/codex-account` - use **official `codex app-server` account APIs** as the managed-account control plane - keep **app-owned API key storage** in the app - explicitly merge the three different truths: - managed ChatGPT account truth from `codex app-server` - API key availability truth from app secure storage and ambient env detection - real execution truth from `codex exec` This feature should become the source of truth for: - autodetect of an already logged-in Codex / ChatGPT account - login / cancel / logout UI flow - plan type display - rate limit display - subscription-first connection copy - launch-readiness policy for ChatGPT-backed Codex - deterministic per-launch auth-mode forcing Core rule: - `codex exec` remains the execution seam - `codex app-server` becomes the account control-plane seam - legacy Codex transport stays deleted - legacy Codex OAuth stays deleted - direct `auth.json` parsing stays forbidden - `chatgptAuthTokens` host-managed mode stays out of scope ## Goals And Non-Goals ### Goals - restore strong Codex subscription UX on top of the native runtime - make managed ChatGPT account truth first-class again in the app - keep API key support without letting it hijack subscription semantics - keep runtime execution on `codex exec` - keep ownership boundaries aligned with `FEATURE_ARCHITECTURE_STANDARD.md` - make rollout safe through additive, testable composition ### Non-goals This feature is **not** trying to: - revive legacy Codex transport - revive legacy Codex OAuth implementation details - parse `~/.codex/auth.json` - add browser-mode local app-server support in the first wave - add app-server-managed API key login in the first wave - add per-member auth preferences or per-member Codex backend preferences - redesign the whole CLI shell UX for other providers - bundle plugin/app-server enrichment beyond account control-plane needs - solve orchestrator parity in the same first implementation ## Glossary These terms are used repeatedly in the plan and must stay consistent. ### Preferred auth mode Persisted user intent. Allowed values: - `auto` - `chatgpt` - `api_key` ### Effective auth mode The auth mode the next launch will actually use after runtime evaluation. Allowed values: - `chatgpt` - `api_key` - `null` ### Snapshot state High-level UI/account state describing what the app currently believes about Codex account availability. Examples: - `managed_account_connected` - `both_available` - `degraded` ### Launch readiness Execution-oriented state used to decide whether the app should launch Codex and under which auth mode. Examples: - `ready_chatgpt` - `ready_api_key` - `missing_auth` ### Managed account A ChatGPT-authenticated Codex account owned and persisted by Codex itself. ### API key availability Whether the app can supply an OpenAI API key from its own secure storage or ambient env detection. ### Degraded A state where the control plane cannot fully verify Codex account truth right now, but the app may still have partial or fresh-enough knowledge to present a careful status and in some cases still launch. ### Last-known-good snapshot The most recent successful account snapshot that came from a real app-server read and passed normal merge and validation logic. ### Freshness window A bounded period during which the feature may temporarily reuse last-known-good managed-account truth while the app-server is degraded. ### Control plane The account-management seam implemented via `codex app-server`. ### Execution plane The actual task-running seam implemented via `codex exec`. ## Chosen Plan Assessment Chosen plan: - full Codex app-server account seam as a dedicated feature slice, while keeping `codex-native` execution Assessment: - `🎯 9 🛡️ 9 🧠 7` - estimated implementation size: `1800-3200` lines in `claude_team`, plus tests and docs ## Top 3 Viable Shapes ### 1. Full app-server account feature slice - chosen `🎯 9 🛡️ 9 🧠 7` Estimated size: `1800-3200` lines Idea: - build a dedicated `codex-account` feature - use `codex app-server` for account state, login lifecycle, and rate limits - keep API keys app-owned - keep execution on `codex exec` Why this wins: - best long-term architecture - truthful subscription UX - avoids legacy transport return - creates a clean seam between account control plane and execution plane - aligns with the repo's feature architecture standard Main cost: - more moving parts than simple CLI probing - requires careful runtime-policy integration ### 2. Hybrid read-via-app-server and login-via-cli `🎯 8 🛡️ 8 🧠 6` Estimated size: `1200-2200` lines Idea: - `account/read` and rate limits via app-server - login/logout still via `codex login` / `codex logout` Pros: - simpler login integration - still gets rich autodetect and plan truth Cons: - split control plane - less internally coherent - more transitional than final ### 3. CLI-only managed-account seam `🎯 8 🛡️ 8 🧠 4` Estimated size: `700-1200` lines Idea: - use `codex login status`, `codex login`, `codex logout` - build UI around plain CLI probing Pros: - simpler - safer if we were optimizing only for speed Cons: - poorer structured account metadata - weak rate-limit surface - less extensible - does not justify a full feature slice as well ## Final Decision We are taking option 1. Reason: - the user requirement is not just "support subscription somehow" - the requirement is "bring back the strong legacy-quality Codex subscription UX, but on the native runtime" - for that requirement, the app-server account seam is the cleanest and most future-proof architecture ## Resolved Decisions Register This section keeps the most important architectural choices explicit, so they do not get re-litigated ad hoc during implementation. ### Resolved - execution seam Decision: - keep execution on raw `codex exec` Why: - it matches the current cutover direction - it avoids reopening the legacy transport question inside this feature ### Resolved - control-plane seam Decision: - use `codex app-server` for account lifecycle and rate-limit truth Why: - it is the official structured surface - it supports managed account autodetect and login lifecycle without file parsing ### Resolved - secret ownership Decision: - ChatGPT managed auth belongs to Codex - API key ownership remains app-owned Why: - avoids dual key stores - keeps responsibility boundaries clear ### Resolved - Codex connection naming Decision: - persisted preference uses `preferredAuthMode: "auto" | "chatgpt" | "api_key"` Why: - `oauth` is legacy wording and no longer the right semantic label for Codex ### Resolved - browser mode Decision: - browser-mode app-server support is deferred Why: - desktop Electron path is the real target for the first implementation - we should not hide platform limitations behind fake parity ### Resolved - device code Decision: - ChatGPT browser flow is first-class - `chatgptDeviceCode` is deferred unless required by a real blocker Why: - browser flow better matches the intended legacy-quality desktop UX ### Resolved - degraded launch policy Decision: - degraded control-plane state may still be launchable only with positive current or sufficiently fresh prior managed-account evidence Why: - prevents false hard blocks - also prevents indefinite stale-account lies ## Problem Statement ### What is wrong today The current codebase has fully cut over to `codex-native` execution, but the account and UX layer was flattened too far. Current product truth: - Codex runtime lane is native-only - Codex UI is effectively API-key-only - Codex managed ChatGPT subscription autodetect is gone from app UX - Codex login/logout from app UI was removed - current launch-readiness policy still assumes API key credentials are required This creates a product and architecture mismatch: - the real Codex native runtime supports ChatGPT account auth - but the app currently acts as if only API keys exist ### Why this is dangerous If we only restore cosmetic UI copy and do not change runtime policy, we will create a worse failure mode: - UI says subscription is available - but launch still fails because the app hard-gates on `OPENAI_API_KEY` or `CODEX_API_KEY` That would be a deceptive product state. So the plan must fix: 1. managed account detection 2. managed login flow 3. runtime launch policy 4. UI presentation all together ## Confirmed Facts And Constraints This section lists facts the plan relies on. ## Official Codex facts Based on the current public OpenAI Codex docs, plus the protocol schemas generated from the installed Codex binary on this machine: - Codex supports both ChatGPT account auth and API key auth - `codex app-server` supports: - `account/read` - `account/login/start` - `account/login/cancel` - `account/logout` - `account/rateLimits/read` - `account/updated` - `account/login/completed` - `account/rateLimits/updated` - `account/read` returns: - `account | null` - `requiresOpenaiAuth` - when `account.type === "chatgpt"`, the current schema requires: - `email` - `planType` - when `account.type === "apiKey"`, the current schema exposes only: - `type` - current generated `account/updated` schema includes nullable: - `authMode` - `planType` - current documented `authMode` values include: - `apikey` - `chatgpt` - `chatgptAuthTokens` - `null` - current generated `account/rateLimits/read` schema exposes: - `rateLimits` - `rateLimitsByLimitId` - per-snapshot `planType | null` - Codex config supports: - `forced_login_method = "chatgpt"` - `forced_login_method = "api"` - Codex tools accept config overrides via top-level `-c key=value` - current app-server docs explicitly show ChatGPT browser flow via: - `type: "chatgpt"` - current app-server docs explicitly show externally managed token mode via: - `type: "chatgptAuthTokens"` - the generated protocol types explicitly mark `chatgptAuthTokens` as unstable / internal-only, so the first implementation must not depend on it Interpretation rule: - steady-state managed-account identity must still come from successful `account/read` - notification fields are freshness accelerators, not a durable replacement read model Sources: - [Authentication - Codex](https://developers.openai.com/codex/auth) - [App Server - Codex](https://developers.openai.com/codex/app-server) ## Locally verified facts Verified on this machine on 2026-04-20: - `codex login status` returns `Logged in using ChatGPT` - `codex app-server` starts locally - `account/read` returned: - `account.type = "chatgpt"` - `email = "quantjumppro@gmail.com"` - `planType = "pro"` - `requiresOpenaiAuth = true` Practical implication: - managed-account autodetect is real - it does not require reverse-engineering auth storage ## Current repo facts Current relevant code facts: - `recent-projects` already uses `codex app-server` over short-lived JSON-RPC stdio sessions - generic Codex runtime status currently sits in: - `src/main/services/runtime/ClaudeMultimodelBridgeService.ts` - generic provider connection logic currently sits in: - `src/main/services/runtime/ProviderConnectionService.ts` - launch env assembly currently sits in: - `src/main/services/runtime/providerAwareCliEnv.ts` - Codex UI currently flattened to API-key-only sits in: - `src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx` - `src/renderer/components/runtime/providerConnectionUi.ts` - `src/renderer/components/dashboard/CliStatusBanner.tsx` - `src/renderer/components/settings/sections/CliStatusSection.tsx` ## Current schema facts Current config and shared-type state: - `providerConnections.codex` is currently `Record` - `configValidation` currently rejects real Codex connection fields except for ignored stale legacy fields - shared `AppConfig` mirrors still declare Codex provider connections as empty Practical implication: - this feature requires deliberate config schema expansion and migration logic ## App-Server Compatibility Facts Current local code and generated protocol schemas show: - `recent-projects` already initializes app-server with: - `experimentalApi: false` - `optOutNotificationMethods` - generated `initialize` response includes: - `codexHome` - `platformFamily` - `platformOs` - generated login response for `type: "chatgpt"` includes: - `loginId` - `authUrl` Practical implication: - the feature can and should stay on the stable app-server surface - compatibility should be decided by an initialize-plus-required-method handshake, not by semver parsing alone - auth-root diagnostics can use `initialize.codexHome` as a first-class observed fact instead of only inferring from environment variables ## Honest Confidence Hotspots These are the places where confidence is lower than the rest of the plan and where we should be deliberately conservative. ### Hotspot 1 - undocumented or lightly documented auth variants Assessment: - `🎯 6 🛡️ 9 🧠 3` What we know: - official app-server docs clearly show: - `chatgpt` - `apiKey` - `chatgptAuthTokens` - local schema / CLI evidence suggests more variants may exist Plan decision: - first implementation depends only on the clearly documented browser-flow `chatgpt` path - do not build phase 1 around additional auth variants Why this is the safe choice: - avoids coupling to protocol surfaces that may be less stable or less publicly specified ### Hotspot 2 - exact ordering of app-server notifications Assessment: - `🎯 7 🛡️ 9 🧠 4` What we know: - docs and local evidence show `account/login/completed` and `account/updated` - but we should not assume stronger guarantees than we have to Plan decision: - notifications accelerate freshness - explicit snapshot refresh remains the recovery source of truth Why this is the safe choice: - even if event order changes slightly, the feature still converges to the correct steady state ### Hotspot 3 - rate-limits payload usefulness for the first UI Assessment: - `🎯 7 🛡️ 8 🧠 4` What we know: - app-server exposes ChatGPT rate-limit reads and updates - exact display value of every field may not be necessary for phase 1 UX Plan decision: - keep rate-limits lazy - present the minimal truthful subset first - avoid making base account UX depend on rate-limit richness Why this is the safe choice: - avoids blocking the critical auth/launch story on secondary UI detail ### Hotspot 4 - `forced_login_method` long-term contract stability Assessment: - `🎯 7 🛡️ 8 🧠 3` What we know: - the auth docs and local behavior support the approach - the CLI reference page is not the strongest canonical place for this exact contract Plan decision: - isolate this override behind the coordinator/env builder seam - do not scatter it through renderer or generic shell logic Why this is the safe choice: - if the exact override mechanism ever changes, the blast radius stays small ### Hotspot 5 - precedence of account fields across read, notification, and rate-limit surfaces Assessment: - `🎯 7 🛡️ 9 🧠 4` What we know: - the generated protocol schema shows `account/updated` carries nullable `authMode` and `planType` - `account/rateLimits/read` also carries `planType` - notifications are best-effort and should not be treated as a durable replay log Plan decision: - the latest successful `account/read` owns steady-state account identity - `account/updated` and rate-limit snapshots may refresh hints and trigger coalesced rereads - they must never synthesize account presence on their own Why this is the safe choice: - avoids phantom login or phantom subscription UI caused by late, partial, or duplicated notifications ### Hotspot 6 - binary compatibility and stable-vs-experimental app-server surface Assessment: - `🎯 8 🛡️ 9 🧠 4` What we know: - official app-server docs describe `initialize.params.capabilities.experimentalApi` - current local `recent-projects` integration already uses `experimentalApi: false` - generated `initialize` response exposes `codexHome`, `platformFamily`, and `platformOs` Plan decision: - first-wave `codex-account` must depend only on stable account APIs - feature readiness must be gated by successful initialize plus required stable method support - do not make product behavior depend on parsing CLI semver strings alone Why this is the safe choice: - reduces risk from partial protocol drift across installed Codex binaries - keeps compatibility logic tied to the actual negotiated surface ### Hotspot 7 - managed workspace restriction behavior in admin-controlled installs Assessment: - `🎯 6 🛡️ 8 🧠 4` What we know: - official auth docs support `forced_chatgpt_workspace_id` - official docs say mismatched credentials cause Codex to log the user out and exit - the app does not currently own workspace selection or workspace switching for Codex Plan decision: - first wave treats workspace restriction as admin policy truth, not as generic missing-auth - the UI must not invent a workspace picker or a fake remediation flow the app does not own Why this is the safe choice: - preserves truthful UX in managed environments without expanding scope into unsupported account management ### Hotspot 8 - trust boundary around `authUrl` and sensitive login metadata Assessment: - `🎯 7 🛡️ 9 🧠 3` What we know: - `account/login/start { type: "chatgpt" }` returns `authUrl` and `loginId` - existing generic `shell:openExternal` allows `http`, `https`, and `mailto` - the feature only needs browser auth URLs, not general-purpose URL opening Plan decision: - keep raw `authUrl` handling in main only - require a stricter feature-specific validation policy before opening: - scheme must be `https` - no renderer round-trip for the raw URL - no raw URL logging - treat `loginId` as process-lifecycle metadata, not as user-facing state Why this is the safe choice: - reduces accidental leakage of login URLs or correlation ids across IPC, logs, and renderer state ### Hotspot 9 - mutation races and app-server session topology Assessment: - `🎯 7 🛡️ 9 🧠 5` What we know: - `account/login/completed`, `account/login/cancel`, `account/logout`, and forced snapshot refreshes can overlap in time - app-server notifications are connection-scoped - `recent-projects` already uses separate short-lived stdio app-server sessions Plan decision: - serialize mutating account operations in the main-process feature - keep the login session on its own dedicated app-server connection - keep passive reads on separate short-lived sessions - let fresh steady-state snapshot truth settle races instead of trusting action intent alone Why this is the safe choice: - avoids cross-session notification bleed, duplicate side effects, and stale mutations reanimating old UI state ## Pre-Implementation Confidence Burn-Down Checklist These checks are the shortest path to reducing the remaining honest uncertainty before or during the first implementation steps. ### Burn-down check 1 - capture real browser-login notification sequence Goal: - observe the real order of: - `account/login/completed` - `account/updated` - any adjacent auth-related notifications Why: - confirms our event-ordering assumptions are conservative enough Expected outcome: - even if order differs slightly from expectation, the explicit refresh model remains valid ### Burn-down check 2 - capture one real `account/rateLimits/read` sample Goal: - verify the actual payload shape we want to expose in the first UI Why: - reduces uncertainty around which fields are worth surfacing in phase F Expected outcome: - confirm minimal truthful fields for initial UI - defer decorative/secondary fields safely ### Burn-down check 3 - re-verify ChatGPT launch path with ambient API keys present Goal: - prove that the chosen env sanitization plus auth override actually forces the intended ChatGPT path Why: - this is the highest-value correctness risk in the whole feature Expected outcome: - capture one signoff artifact that launch uses ChatGPT semantics even when API-key env vars are present in the parent shell ### Burn-down check 4 - verify logout semantics against stale cached state Goal: - prove that explicit logout wins over last-known-good snapshot reuse Why: - prevents the most embarrassing stale-account resurrection bug Expected outcome: - logout clears managed-account truth immediately - degraded follow-up reads cannot resurrect it ### Burn-down check 5 - confirm app-server and exec share identical auth-store roots Goal: - compare resolved `HOME`, `USERPROFILE`, and `CODEX_HOME` across both paths Why: - split auth store is a high-severity, low-visibility failure mode Expected outcome: - one centralized env normalization path is sufficient for both control plane and execution plane ### Burn-down check 6 - capture one real `account/updated` payload sequence Goal: - observe whether `authMode` and `planType` arrive as expected across login, logout, and steady state refreshes Why: - confirms the precedence rules stay conservative against nullable or partial notification payloads Expected outcome: - keep `account/read` as the steady-state owner - use notifications only as accelerators and invalidation hints ### Burn-down check 7 - verify initialize handshake on the stable surface Goal: - confirm the feature can derive a compatibility verdict from stable initialize plus required method support Why: - prevents false-ready UI on installs where `codex app-server` exists but the required account surface is too old or otherwise incompatible Expected outcome: - initialize succeeds with `experimentalApi: false` - the feature records `codexHome`, `platformFamily`, and `platformOs` for diagnostics - unsupported/mismatched installs become `app-server-incompatible`, not generic auth failure ### Burn-down check 8 - capture one real ChatGPT login URL handling sample Goal: - validate the actual `authUrl` scheme/shape and prove the browser-open path can stay main-only Why: - closes the last trust-boundary uncertainty around login URL handling without expanding scope into brittle host-specific assumptions Expected outcome: - the real login URL is `https` - raw `authUrl` never needs to cross IPC - logs can record only redacted/derived diagnostics such as scheme and hostname when necessary ### Burn-down check 9 - exercise cancel/completion/logout race resolution Goal: - prove that overlapping account mutations still converge to one truthful steady state Why: - this is one of the easiest places to create zombie pending state or accidental false logout Expected outcome: - cancel followed by late login completion does not force a false disconnect - logout during pending login still ends in logged-out truth - stale pre-mutation reads cannot overwrite post-mutation state ## Recommended Placement Of Burn-Down Checks In The Phase Plan To keep momentum, we should not treat all uncertainty as a separate research project. ### Before or during Phase B - Burn-down check 1 - Burn-down check 2 - Burn-down check 6 - Burn-down check 7 - Burn-down check 8 Reason: - these directly shape contracts and event handling ### Before or during Phase D - Burn-down check 3 - Burn-down check 5 Reason: - these directly shape launch correctness and env routing ### Before or during Phase E - Burn-down check 4 - Burn-down check 9 Reason: - this directly shapes logout semantics, mutation ordering, and stale-state invalidation ## Uncertainty Triage - What Must Be Settled Before Broad Coding Not every unknown deserves to block the feature. The plan should distinguish hard preconditions from safe rollout follow-ups. ### Must be explicit before the feature spreads across shell and renderer - account field precedence between `account/read`, `account/updated`, and `account/rateLimits/read` - stable-surface compatibility gate for installed `codex app-server` - containment rules for `authUrl`, `loginId`, and account email across main, IPC, and logs - mutation serialization and race-settlement policy for login/cancel/logout - shared auth-store root resolution for app-server and `codex exec` - launch-time env sanitization when ambient API keys exist - logout invalidation semantics and last-known-good clearing rules Why: - each of these can create silent false truth in UI or billing/auth mismatches at runtime ### Can be refined during rollout without invalidating the architecture - how rich the first rate-limit UI should be - whether degraded state needs an extra badge in addition to text - whether we surface plan-type changes instantly from notifications or only after follow-up reads - how aggressively background refresh should coalesce under bursty notification traffic - whether managed-workspace restriction gets a dedicated visual badge or text-only treatment Why: - these change UX sharpness, not the core correctness contract ## Known Current Mismatches The Plan Must Explicitly Eliminate These are not abstract concerns. They already exist in the current code and must be treated as first-class implementation targets. ### Mismatch 1 - Codex connection truth is still API-key-first Current reality: - `ProviderConnectionService` still treats Codex readiness as "API key exists" - `getConfiguredConnectionIssue()` still says Codex native requires `OPENAI_API_KEY` or `CODEX_API_KEY` Why this is dangerous: - once ChatGPT account UX is restored, launch policy can still lie and hard-fail incorrectly ### Mismatch 2 - shell UI copy still flattens Codex to API key management Current reality: - `providerConnectionUi.ts` still uses: - `Configure API key` - `Saved API key available in Manage` - `Codex native ready` - there is no first-class Codex managed account summary Why this is dangerous: - the renderer will keep presenting the wrong mental model even if the runtime becomes correct ### Mismatch 3 - shell login/logout flow is terminal-modal based Current reality: - `CliStatusBanner.tsx` - `CliStatusSection.tsx` still drive provider login/logout through terminal modals and shell commands. Why this is dangerous: - even if app-server account logic exists, the visible UX would still route through the wrong seam ### Mismatch 4 - config schema has no real Codex connection preference Current reality: - `providerConnections.codex` is still effectively empty - validation only tolerates stale legacy keys instead of modeling the current desired state Why this is dangerous: - renderer state, persistence, and launch policy can drift because there is no canonical stored preference ### Mismatch 5 - app-server infrastructure is feature-owned by `recent-projects` Current reality: - generic JSON-RPC stdio transport is currently nested under `recent-projects` Why this is dangerous: - a second feature would either duplicate the transport or deep-import another feature's internals ### Mismatch 6 - current multimodel shell status cannot be the Codex account source of truth Current reality: - `CliProviderStatus` is useful for binary/backend/model truth - it is not sufficient for login lifecycle, plan display, or dual-surface auth truth Why this is dangerous: - forcing all Codex account truth into `CliProviderStatus` would create a leaky, provider-specific blob in generic shell status contracts ## Hard constraints The implementation must respect all of the following: 1. Execution must stay on `codex-native` / `codex exec`. 2. The feature must not recreate legacy Codex transport. 3. The feature must not parse `~/.codex/auth.json`. 4. The feature must not duplicate API key storage responsibility into Codex-managed storage. 5. The feature must not hard-block launch only because app-server is transiently degraded. 6. The feature must keep auth truth and execution truth separate. 7. The feature must keep app-server and `codex exec` running against the same auth storage context. ## Why This Must Be A Feature Slice This is not just another Codex-specific if-statement. This work: - spans `main -> preload -> renderer` - owns transport wiring - owns its own use cases - owns provider-specific business policy - is expected to grow That matches the feature standard's criteria for a full slice. So the implementation should **not** be buried into: - `ProviderConnectionService` - `ProviderRuntimeSettingsDialog` - `CliStatusBanner` - `CliStatusSection` Those shell modules should consume this feature, not own it. ## Feature Topology ```text src/features/codex-account/ contracts/ api.ts channels.ts dto.ts events.ts index.ts core/ domain/ CodexConnectionPreference.ts CodexManagedAccount.ts CodexLaunchReadiness.ts CodexConnectionSnapshot.ts CodexLoginState.ts application/ ports/ CodexManagedAccountSourcePort.ts CodexManagedLoginPort.ts CodexRateLimitSourcePort.ts CodexApiKeyAvailabilityPort.ts CodexBinaryResolverPort.ts CodexShellEnvPort.ts BrowserLauncherPort.ts ClockPort.ts LoggerPort.ts use-cases/ GetCodexConnectionSnapshotUseCase.ts RefreshCodexConnectionSnapshotUseCase.ts StartCodexChatgptLoginUseCase.ts CancelCodexLoginUseCase.ts LogoutCodexManagedAccountUseCase.ts ReadCodexRateLimitsUseCase.ts EvaluateCodexLaunchReadinessUseCase.ts main/ composition/ createCodexAccountFeature.ts adapters/ input/ ipc/registerCodexAccountIpc.ts output/ presenters/ CodexConnectionSnapshotPresenter.ts CodexRateLimitsPresenter.ts CodexAccountEventPresenter.ts runtime/ ProviderConnectionApiKeySourceAdapter.ts CodexLaunchReadinessRuntimeAdapter.ts shell/ ElectronBrowserLauncherAdapter.ts infrastructure/ cache/ InMemoryCodexAccountCache.ts codex/ CodexAccountAppServerClient.ts CodexLoginSessionManager.ts CodexAccountEnvBuilder.ts preload/ createCodexAccountBridge.ts index.ts renderer/ index.ts adapters/ codexAccountViewModel.ts codexProviderShellAdapter.ts hooks/ useCodexAccount.ts useCodexLoginFlow.ts useCodexRateLimits.ts ui/ CodexAccountConnectionPanel.tsx CodexLoginPendingPanel.tsx CodexRateLimitsPanel.tsx CodexConnectionSummaryBadge.tsx ``` ## Responsibility Split By Layer ### `contracts/` Contains: - DTOs - event payloads - IPC channel names - preload API contract Must not contain: - Electron APIs - runtime policy - child-process details ### `core/domain/` Contains: - preference model - managed-account model - launch-readiness model - invariants for combining account and API key truth Must not contain: - `ipcRenderer` - `electron` - shell env access - JSON-RPC transport ### `core/application/` Contains: - use cases - ports - merge rules - state transition rules Must not contain: - actual app-server spawn logic - actual browser open logic - app config singleton access ### `main/composition/` Contains: - wiring of ports to infrastructure - export of a small feature facade ### `main/adapters/input/` Contains: - IPC registration only ### `main/adapters/output/` Contains: - translation from existing shell services into feature ports - presenters for IPC-safe DTOs ### `main/infrastructure/` Contains: - app-server stdio JSON-RPC details - login session lifecycle management - env sanitization and assembly - cache implementation ### `preload/` Contains: - bridge methods and event subscriptions ### `renderer/` Contains: - hooks - view-model mapping - Codex-specific UI pieces ## Feature Facade And Public Contract Shape To keep SRP and interface segregation intact, the feature should expose a small facade rather than letting shell code reach into individual infrastructure pieces. Recommended main-process facade shape: ```ts interface CodexAccountFeatureFacade { getSnapshot(options?: { forceFresh?: boolean }): Promise; refreshSnapshot(): Promise; startChatgptLogin(): Promise; cancelLogin(): Promise; logout(): Promise; getRateLimits(options?: { forceFresh?: boolean }): Promise; evaluateLaunchReadiness(options: { binaryPath?: string | null; preferredAuthMode?: 'auto' | 'chatgpt' | 'api_key' | null; }): Promise; subscribe(listener: (event: CodexAccountEventDto) => void): () => void; } ``` Design rule: - the shell should depend on this facade or its IPC equivalent - the shell should not call: - `CodexAccountAppServerClient` - `CodexLoginSessionManager` - cache objects - low-level env builders This keeps the implementation open for extension without forcing broad shell rewrites later. ## Shared Infrastructure Extraction Plan We already have generic app-server transport primitives hiding under `recent-projects`. That code should be extracted before `codex-account` is built, otherwise `recent-projects` becomes the owner of unrelated infrastructure. Recommended extraction target: ```text src/main/services/infrastructure/codex-app-server/ JsonRpcStdioClient.ts CodexAppServerSessionFactory.ts codexAppServerDefaults.ts ``` What gets extracted: - generic stdio JSON-RPC client - generic initialize/initialized session bootstrap - default request timeout values - default suppressed notification configuration What stays inside `recent-projects`: - thread-list request logic - recent-projects-specific source adapter What stays inside `codex-account`: - account request logic - login lifecycle logic - rate-limits logic Important DRY rule: - extract transport primitives - do **not** create one giant "CodexService" that mixes unrelated product features ## Architecture Decision On Sources Of Truth This feature only works if source-of-truth boundaries are explicit. ## Truth 1 - managed account truth Source: - `codex app-server account/read` Used for: - whether a managed ChatGPT account exists - account email - plan type - account auth mode ## Truth 1a - field precedence inside managed account truth The feature must not treat every app-server field as equally authoritative. Precedence rules: 1. `account/read` owns steady-state identity: - whether an account exists - `account.type` - `email` - baseline `planType` - `requiresOpenaiAuth` 2. `account/updated.authMode` may update an observed auth hint immediately, but must not by itself create or delete a managed account. 3. `account/updated.planType` may refresh displayed subscription metadata when present, but if it arrives as `null` or arrives without a known account, the feature should trigger a coalesced follow-up `account/read` before changing steady-state account identity. 4. `account/rateLimits/read.planType` may corroborate subscription UI, but must not create account presence or override a newer successful `account/read`. 5. explicit logout clears managed-account truth synchronously before any background refresh result is accepted. 6. degraded reads may reuse last-known-good account truth only within the freshness window and only if no explicit logout happened after that snapshot. Implementation consequence: - `planType` is a field with precedence and fallback rules - it is not a standalone durable source of truth from notifications alone ## Truth 2 - API key truth Source: - existing app API key storage plus ambient env detection Used for: - whether the app can launch Codex using API key mode - whether an app-managed OpenAI key is stored - where the key comes from ## Truth 3 - execution truth Source: - `codex exec` Used for: - whether a real Codex run starts - real failure or success at execution time ## Truth 4 - renderer shell status truth Source: - composed presentation model from: - generic runtime provider status - Codex account feature snapshot Important design choice: - we should **not** force all Codex account fields into `CliProviderStatus` - `CliProviderStatus` remains the source of truth for: - binary/runtime/backend/model status - the feature snapshot remains the source of truth for: - managed account - preferred and effective auth mode - launch readiness - login lifecycle - rate limits The shell must compose these two bounded contexts at presentation time. This is not "two conflicting sources of truth". It is: - one runtime status context - one account status context ## Ownership Matrix This table is the practical anti-bug contract for the feature. | Concern | Canonical source | Owning layer | Persisted? | Cache TTL | Used by | Must not be inferred from | | --- | --- | --- | --- | --- | --- | --- | | Codex binary path / binary installed | existing multimodel runtime status | shell runtime services | no | existing shell TTL | shell cards, launch gating | `codex-account` snapshot | | Codex backend lane | existing runtime config `runtime.providerBackends.codex` | shell runtime services | yes | n/a | launch, status, provisioning | account auth state | | Codex preferred auth mode | `providerConnections.codex.preferredAuthMode` | `codex-account` feature | yes | n/a | account panel, launch policy | `CliProviderStatus.authMethod` | | Managed account presence | `account/read` | `codex-account` feature | no | 3-10s | account panel, readiness | local file parsing | | Managed account email | `account/read` | `codex-account` feature | no | 3-10s | account panel | renderer local state | | Managed account plan type | latest successful `account/read`; nullable refresh hints from `account/updated` and `account/rateLimits/read` | `codex-account` feature | no | 3-10s | subscription UI, rate limits | static plan assumptions or notification-only state | | Requires OpenAI auth | `account/read.requiresOpenaiAuth` | `codex-account` feature | no | 3-10s | readiness messaging | provider id alone | | API key availability | app secure storage plus ambient env detection | provider connection adapter | no | 0-3s | readiness, secondary badges | app-server account state | | Effective auth mode for next launch | `EvaluateCodexLaunchReadinessUseCase` | `codex-account` feature | no | request-scoped | launch env builder | raw config alone | | Login pending state | `CodexLoginSessionManager` | `codex-account` feature | no | live | UI pending panels | cached snapshot | | Rate limits | `account/rateLimits/read` | `codex-account` feature | no | 30-60s | account detail panel | plan type alone | | Final execution success / failure | `codex exec` | runtime execution lane | no | live | launch result UX | account snapshot | Implementation rule: - if a field is not canonical in this table, the layer may display it but must not own it ## Dependency Direction And Anti-Corruption Rules To stay aligned with `FEATURE_ARCHITECTURE_STANDARD.md`, the new feature must preserve these directions: 1. `renderer` depends on: - `@features/codex-account/renderer` - `@features/codex-account/contracts` 2. `preload` depends on: - `@features/codex-account/contracts` 3. `main shell` depends on: - `@features/codex-account/main` 4. `codex-account/core/*` depends only on: - feature-local ports and domain models 5. `codex-account/main/infrastructure/*` may depend on: - Electron - child process / stdio - shared shell env helpers 6. `recent-projects` must not become a transitive dependency of `codex-account` Anti-corruption rule: - the feature may adapt values out of `CliProviderStatus` - it must not reshape its domain around `CliProviderStatus` This is important because `CliProviderStatus` is a generic shell contract, not the Codex account domain model. ## Dependency Enforcement Recommendations The architecture standard already gives the broad rules. For this feature, we should make the most important ones operational. ### Recommended import discipline - shell code imports only: - `@features/codex-account/main` - `@features/codex-account/contracts` - `@features/codex-account/renderer` - `@features/codex-account/preload` - tests may deep-import internals when needed - production code outside the feature should not deep-import internal adapters, ports, or infrastructure ### Recommended lint / review guardrails Watch specifically for: - `src/renderer/components/*` importing feature `main/*` - feature `core/*` importing `electron`, child-process modules, or shell services - unrelated features importing `codex-account/main/infrastructure/*` - `recent-projects` becoming a transport owner again through back references ### Practical enforcement rule If a shell file needs a new Codex-specific detail and that detail is not in the feature facade or contracts yet: - extend the facade or contracts - do not bypass the boundary with a deep import ## Explicitly forbidden truth sources - `~/.codex/auth.json` - legacy OAuth state - terminal output parsing as the steady-state account model - `chatgptAuthTokens` - stale renderer-only cached assumptions ## Domain Model ## Connection preference New persisted preference: - `auto` - `chatgpt` - `api_key` Recommended config key: - `providerConnections.codex.preferredAuthMode` Important decision: - do **not** reuse legacy `oauth` naming for Codex Reason: - `oauth` is legacy wording from the old implementation - `chatgpt` maps much more clearly to the new managed-account seam ## Effective auth mode Resolved per snapshot: - `chatgpt` - `api_key` - `null` This can differ from user preference when: - preferred mode is `auto` - one surface is unavailable ## Snapshot state Recommended states: - `runtime_missing` - `checking` - `not_connected` - `managed_account_connected` - `api_key_available` - `both_available` - `login_in_progress` - `logout_in_progress` - `degraded` ## Launch readiness Recommended states: - `ready_chatgpt` - `ready_api_key` - `ready_both` - `warning_degraded_but_launchable` - `missing_auth` - `runtime_missing` ## Domain invariants 1. A managed ChatGPT account and an API key can both exist simultaneously. 2. API key presence must not erase managed account metadata. 3. Managed account presence must not erase API key availability. 4. Launch policy must resolve one effective auth mode per run. 5. Account metadata is display state, not secret state. 6. Login state is transient and process-owned. 7. A degraded app-server read must not automatically mean "logged out". ## State Resolution Matrix This matrix is the fastest way to keep renderer, use cases, and runtime policy aligned. | Binary available | Managed account | API key available | App-server health | Preferred auth | Snapshot state | Launch readiness | Effective auth mode | UI headline | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | no | any | any | any | any | `runtime_missing` | `runtime_missing` | `null` | Codex runtime missing | | yes | no | no | healthy | auto | `not_connected` | `missing_auth` | `null` | Connect ChatGPT account or add API key | | yes | yes | no | healthy | auto | `managed_account_connected` | `ready_chatgpt` | `chatgpt` | ChatGPT account connected | | yes | no | yes | healthy | auto | `api_key_available` | `ready_api_key` | `api_key` | API key available | | yes | yes | yes | healthy | auto | `both_available` | `ready_both` | `chatgpt` | ChatGPT account connected - API key also available | | yes | yes | yes | healthy | chatgpt | `both_available` | `ready_chatgpt` | `chatgpt` | ChatGPT account preferred | | yes | yes | yes | healthy | api_key | `both_available` | `ready_api_key` | `api_key` | API key preferred - ChatGPT account also connected | | yes | yes | no | degraded | auto or chatgpt | `degraded` | `warning_degraded_but_launchable` | `chatgpt` | ChatGPT account detected - unable to fully verify right now | | yes | no | yes | degraded | auto or api_key | `degraded` | `ready_api_key` | `api_key` | API key available - account status degraded | | yes | no | no | degraded | auto | `degraded` | `missing_auth` or warning only if last good account still fresh | `null` or last-good-derived | Unable to verify Codex account state | Interpretation rule: - `snapshot state` is broader UX truth - `launch readiness` is stricter execution truth - they are related but must not be collapsed into one boolean ## Managed Workspace Restriction Policy Official Codex auth/config docs allow administrators to set: - `forced_login_method` - `forced_chatgpt_workspace_id` Docs also state that if active credentials do not match the configured restriction, Codex logs the user out and exits. ### First-wave product policy - do not add a workspace picker or workspace-switch UI in this feature - do not pretend the app can resolve admin-managed workspace policy on the user's behalf - do surface workspace restriction as a distinct normalized policy state when detected ### Expected UX treatment If the installed Codex runtime is restricted to another ChatGPT workspace: - do not show generic `Connect ChatGPT account` copy as the only explanation - do not claim the subscription is simply missing - show an admin-policy-oriented message such as: - `This Codex installation is restricted to a different ChatGPT workspace` ### Architecture implication Workspace restriction should be modeled as: - a normalized error/policy category - potentially `not_connected` or `missing_auth` at the raw launch-readiness layer - but with policy-specific renderer messaging This avoids exploding the core state machine while still keeping the UX honest. ## Config And Migration Plan ## New config shape Update: - `ConfigManager.ProviderConnectionsConfig` - shared `AppConfig` - config validation - settings reset defaults Recommended new shape: ```ts providerConnections: { anthropic: { authMode: 'auto' | 'oauth' | 'api_key' }, codex: { preferredAuthMode: 'auto' | 'chatgpt' | 'api_key' } } ``` ## Migration rules On config read: 1. If `providerConnections.codex.preferredAuthMode` is missing: - default to `auto` 2. If stale legacy `providerConnections.codex.authMode === 'oauth'`: - migrate to `preferredAuthMode = 'chatgpt'` 3. If stale legacy `providerConnections.codex.authMode === 'api_key'`: - migrate to `preferredAuthMode = 'api_key'` 4. If stale legacy `providerConnections.codex.authMode === 'auto'`: - migrate to `preferredAuthMode = 'auto'` 5. Ignore `apiKeyBetaEnabled` completely after migration 6. Write back only the new shape ## Migration algorithm - exact behavior This part needs to be exact because configuration drift is one of the easiest ways to create hard-to-debug launch mismatches. ### Read-time normalization order When config is loaded: 1. load raw JSON from disk 2. apply generic config defaults 3. normalize `runtime.providerBackends.codex` 4. normalize `providerConnections.codex` 5. validate the normalized shape 6. persist normalized config only if it changed materially ### Exact Codex connection normalization rules Given `raw.providerConnections.codex`: - if it is missing or not an object: - replace with `{ preferredAuthMode: "auto" }` - if `preferredAuthMode` exists and is one of: - `auto` - `chatgpt` - `api_key` - keep it - otherwise, inspect stale keys in this order: - `authMode === "oauth"` -> `preferredAuthMode = "chatgpt"` - `authMode === "api_key"` -> `preferredAuthMode = "api_key"` - `authMode === "auto"` -> `preferredAuthMode = "auto"` - everything else -> `preferredAuthMode = "auto"` Then: - drop `authMode` - drop `apiKeyBetaEnabled` - drop any unknown Codex connection keys ### Backward-compatibility rule Old configs must be: - readable - normalizable - writable into the new shape but they must not keep legacy Codex connection fields alive after one clean save cycle. ### Migration idempotency and corrupt-input hardening rules Normalization must be safe under repeated reads and partially corrupted user config. Rules: 1. repeated load -> normalize -> save cycles must converge to the same Codex connection subtree 2. non-object `providerConnections.codex` values must normalize to: - `{ preferredAuthMode: "auto" }` 3. malformed or unknown Codex connection keys must not block app startup 4. already-normalized config should not be rewritten just because the normalizer ran again 5. backup/restore and import paths must reuse the same Codex normalizer instead of re-implementing migration logic separately Practical consequence: - migration is a deterministic cleanup step, not an ongoing source of config churn ### Migration safety rule Config migration must never infer: - "user prefers API key" merely because an API key exists - "user prefers ChatGPT" merely because a managed account exists Preference is persisted user intent. Availability is runtime-observed fact. They must remain distinct. ## Validation rules `configValidation` must: - accept only `preferredAuthMode` under `providerConnections.codex` - accept values: - `auto` - `chatgpt` - `api_key` - tolerate stale legacy keys during migration path only if they are normalized before persistence ## Critical Runtime Policy This is the highest-risk part of the feature. ### Why current policy is wrong Today Codex launch policy effectively means: - if no `OPENAI_API_KEY` or `CODEX_API_KEY`, Codex launch is treated as not ready That is incompatible with the desired product once ChatGPT-managed auth comes back. ### New launch policy The feature must own launch-readiness evaluation. Inputs: - Codex binary availability - managed account presence - API key availability - preferred auth mode - app-server health Outputs: - launch readiness state - effective auth mode - env mutation instructions - user-facing advisory message ### Policy rules 1. If preferred mode is `chatgpt` and managed account exists: - launch is ready - effective auth mode is `chatgpt` 2. If preferred mode is `api_key` and API key exists: - launch is ready - effective auth mode is `api_key` 3. If preferred mode is `auto`: - prefer ChatGPT when managed account exists - otherwise use API key when available - otherwise missing-auth 4. If app-server is degraded: - do not hard-fail launch automatically unless there is explicit evidence auth is absent - return warning-level degraded readiness where appropriate ### Why app-server degradation must not hard-block launch `codex app-server` is an account control-plane seam. It is **not** the execution seam. A transient app-server issue must not cause the app to say: - "Codex cannot launch" when `codex exec` itself could still work. That would create a false negative and a serious product bug. ## Launch Policy Decision Table This table should directly drive the implementation of `EvaluateCodexLaunchReadinessUseCase`. | Preferred auth | Managed account detected | API key available | App-server degraded | Resulting readiness | Effective auth | Required exec env policy | User-facing message | | --- | --- | --- | --- | --- | --- | --- | --- | | `chatgpt` | yes | any | no | `ready_chatgpt` | `chatgpt` | strip API keys, `forced_login_method="chatgpt"` | Launch using ChatGPT account | | `chatgpt` | yes | any | yes | `warning_degraded_but_launchable` | `chatgpt` | strip API keys, `forced_login_method="chatgpt"` | ChatGPT account detected - verification degraded | | `chatgpt` | no | yes | no | `missing_auth` | `null` | no launch | Preferred ChatGPT account is not connected | | `chatgpt` | no | no | no | `missing_auth` | `null` | no launch | Connect a ChatGPT account to use the selected auth mode | | `api_key` | any | yes | any | `ready_api_key` | `api_key` | inject key, `forced_login_method="api"` | Launch using API key | | `api_key` | any | no | any | `missing_auth` | `null` | no launch | Add an API key to use the selected auth mode | | `auto` | yes | yes | no | `ready_both` | `chatgpt` | strip API keys, `forced_login_method="chatgpt"` | Auto selected ChatGPT account | | `auto` | yes | no | no | `ready_chatgpt` | `chatgpt` | strip API keys, `forced_login_method="chatgpt"` | Auto selected ChatGPT account | | `auto` | no | yes | no | `ready_api_key` | `api_key` | inject key, `forced_login_method="api"` | Auto selected API key | | `auto` | yes | yes | yes | `warning_degraded_but_launchable` | `chatgpt` | strip API keys, `forced_login_method="chatgpt"` | Auto selected ChatGPT account - account verification degraded | | `auto` | no | yes | yes | `ready_api_key` | `api_key` | inject key, `forced_login_method="api"` | Auto selected API key - account verification degraded | | `auto` | no | no | yes | `missing_auth` unless last-good account freshness rule applies | `null` | no launch | Unable to verify Codex authentication | Important interpretation: - degraded app-server state does not grant permission to guess a missing managed account forever - the only acceptable degraded-launch case is when there is positive current or sufficiently fresh prior evidence that the managed account exists ## Core Algorithm Sketches These sketches are intentionally close to implementation logic so that multiple contributors do not invent subtly different policies. ### Snapshot merge algorithm - pseudocode ```ts function buildCodexSnapshot(input: { binaryAvailable: boolean; preferredAuthMode: 'auto' | 'chatgpt' | 'api_key'; managedAccountResult: | { kind: 'success'; account: ManagedAccount | null; requiresOpenaiAuth: boolean } | { kind: 'degraded'; reason: string }; apiKeyAvailability: ApiKeyAvailability; loginState: LoginState; lastKnownGoodManagedAccount: ManagedAccount | null; lastKnownGoodObservedAt: number | null; now: number; freshnessWindowMs: number; }): CodexConnectionSnapshot { if (!input.binaryAvailable) { return runtimeMissingSnapshot(input.preferredAuthMode, input.loginState, input.now); } const managedContext = resolveManagedAccountContext({ managedAccountResult: input.managedAccountResult, lastKnownGoodManagedAccount: input.lastKnownGoodManagedAccount, lastKnownGoodObservedAt: input.lastKnownGoodObservedAt, now: input.now, freshnessWindowMs: input.freshnessWindowMs, }); return mergeManagedAndApiKeyTruth({ preferredAuthMode: input.preferredAuthMode, managedContext, apiKeyAvailability: input.apiKeyAvailability, loginState: input.loginState, now: input.now, }); } ``` ### Launch readiness algorithm - pseudocode ```ts function evaluateLaunchReadiness(snapshot: CodexConnectionSnapshot): CodexLaunchReadiness { if (!snapshot.binaryAvailable) { return { state: 'runtime_missing', effectiveAuthMode: null, message: 'Codex runtime missing' }; } switch (snapshot.preferredAuthMode) { case 'chatgpt': if (snapshot.managedAccount?.type === 'chatgpt') { return snapshot.state === 'degraded' ? degradedChatgptReady() : chatgptReady(); } return missingChatgptAuth(); case 'api_key': return snapshot.apiKey.available ? apiKeyReady() : missingApiKeyAuth(); case 'auto': if (snapshot.managedAccount?.type === 'chatgpt') { return snapshot.state === 'degraded' ? degradedChatgptReady() : (snapshot.apiKey.available ? readyBoth() : chatgptReady()); } if (snapshot.apiKey.available) { return apiKeyReady(); } return missingAnyAuth(); } } ``` ### Account event reconciliation algorithm - pseudocode ```ts function onAccountUpdated(event: { authMode: AuthMode | null; planType: PlanType | null }) { cache.lastObservedAuthMode = event.authMode ?? cache.lastObservedAuthMode ?? null; if (event.planType !== null) { cache.lastObservedPlanTypeHint = event.planType; } scheduleCoalescedSnapshotRefresh('account-updated'); } ``` Critical rule: - notification handlers must not clear `managedAccount` only because `authMode` or `planType` arrives as `null` - destructive transitions belong to explicit logout handling and successful follow-up `account/read` results ### Exec env mutation algorithm - pseudocode ```ts function buildExecEnv(baseEnv: Env, readiness: CodexLaunchReadiness, apiKey: string | null): Env { const env = { ...baseEnv }; if (readiness.effectiveAuthMode === 'chatgpt') { delete env.OPENAI_API_KEY; delete env.CODEX_API_KEY; env.CODEX_FORCED_LOGIN_METHOD = 'chatgpt'; return env; } if (readiness.effectiveAuthMode === 'api_key') { if (apiKey) { env.OPENAI_API_KEY = apiKey; env.CODEX_API_KEY = apiKey; } env.CODEX_FORCED_LOGIN_METHOD = 'api'; return env; } return env; } ``` Implementation note: - the real implementation should pass `forced_login_method` via Codex config override arguments, not invent a new environment variable contract - the pseudocode uses a symbolic field only to make the decision process readable ## Extremely important env semantics This section is subtle and non-optional. ### Problem 1 - ambient API keys can poison managed-account autodetect If an app-server child process inherits: - `OPENAI_API_KEY` - `CODEX_API_KEY` then account reads may reflect API-key auth instead of the managed ChatGPT account the user expects to see. ### Problem 2 - execution can silently use the wrong auth surface If ChatGPT mode is selected but execution still inherits: - `OPENAI_API_KEY` - `CODEX_API_KEY` then the run may silently go through API-key auth. That creates the worst possible bug: - UI says subscription is active - but runtime actually bills via API key ### Required env policy For **managed-account control-plane sessions**: - sanitize `OPENAI_API_KEY` - sanitize `CODEX_API_KEY` - do not set `forced_login_method` - preserve the same resolved auth storage context as execution Reason: - control-plane reads should discover unbiased managed-account truth For **execution sessions in `chatgpt` mode**: - sanitize `OPENAI_API_KEY` - sanitize `CODEX_API_KEY` - pass `-c forced_login_method="chatgpt"` For **execution sessions in `api_key` mode**: - inject the resolved API key env - pass `-c forced_login_method="api"` For **execution sessions in `auto -> chatgpt` resolution**: - same as `chatgpt` For **execution sessions in `auto -> api_key` resolution**: - same as `api_key` ### Shared auth storage context rule `account/read`, login, logout, and `codex exec` must use the same resolved: - `HOME` - `USERPROFILE` - `CODEX_HOME` or the UI can observe one auth store while execution uses another. That would create a second class of severe bug: - UI sees a logged-in account - but `codex exec` runs against a different auth store and fails So the feature must centralize Codex env resolution. ## Platform-Specific Runtime Notes This feature is cross-process and cross-platform enough that platform rules should be explicit. ### Auth-store env precedence Recommended precedence for auth-store-related env resolution: 1. explicit per-call overrides provided by the app 2. resolved interactive shell env 3. process env fallback ### `HOME`, `USERPROFILE`, `CODEX_HOME` Rules: - always resolve a single canonical auth root - materialize both `HOME` and `USERPROFILE` consistently to avoid child-process drift - preserve an explicit `CODEX_HOME` if one exists - do not silently invent different auth roots for app-server vs exec Diagnostics rule: - when available, compare the app's resolved auth root with `initialize.codexHome` - treat disagreement as a first-class diagnostic signal, not as a silent implementation detail ### Windows nuance On Windows, child processes may consult `USERPROFILE` even when `HOME` is also set. Implementation implication: - the env builder should not treat `HOME` alone as sufficient on Windows-capable flows ### macOS and Linux nuance On Unix-like systems, `HOME` is usually the primary root. Implementation implication: - if `USERPROFILE` is absent, we should still fill it consistently once we have a canonical root so both app-server and exec see the same store contract ### Browser-launch nuance Desktop login should use the Electron/browser-launch adapter, not shell command guessing. Implementation implication: - browser opening belongs to the feature infrastructure adapter - it should not be scattered across banner/section/dialog components ### PATH and shell env nuance The feature should inherit enough environment to find the Codex binary and preserve normal shell behavior, but auth-critical env values must still be rewritten deterministically. Implementation implication: - preserve normal shell-derived env where safe - sanitize only the auth-critical variables the policy explicitly owns ## Concurrency, Ordering, And Race Policy This section is necessary because the feature will combine: - cached reads - explicit refresh - login notifications - logout actions - shell status refreshes Without a strict ordering contract, the UI can easily regress into stale or contradictory state. ### Snapshot sequencing rule Every refresh path should carry: - `requestId` - `startedAt` - `observedAt` The feature should only publish a snapshot if it is newer than the last settled snapshot for the same source epoch. Practical rule: - a slow degraded read must not overwrite a newer successful read - a pre-login cached snapshot must not overwrite a post-login snapshot - a pre-logout cached snapshot must not overwrite a post-logout snapshot ### Single-flight rule Passive snapshot refreshes from: - dashboard banner - settings dialog - provider card refresh must collapse into one in-flight promise per main-process feature instance. ### Login lifecycle exclusivity rule At most one login session may be live at a time. If the user clicks login repeatedly: - return the current pending login state - do not start parallel app-server login sessions ### Renderer subscription rule Renderer hooks must treat `snapshot-updated` and `login-state-changed` as additive feature events, not as implicit replacement for shell runtime status. This avoids one subtle class of bug: - account snapshot event arrives - shell status is still loading - UI accidentally treats feature snapshot as full provider status ### Cache invalidation rule Invalidate snapshot cache immediately on: - login start - login completion - logout success - preferred auth mode change - explicit manual refresh Do not wait for TTL expiry after user-initiated auth transitions. ## Freshness Window And Last-Known-Good Policy This is intentionally separated from generic cache policy because it affects safety semantics, not just performance. ### Recommended default - keep a `lastKnownGoodManagedAccountSnapshot` - keep a `lastKnownGoodObservedAt` - use a default freshness window around `60 seconds` Why not longer by default: - too long and the app starts lying after real logout or auth expiry Why not shorter by default: - too short and transient app-server failures collapse the UX into false logout too easily ### What may be carried forward During a degraded control-plane read, the feature may carry forward only: - managed account presence - managed account email - managed account plan type - effective auth mode only if it was derived from the managed account path ### What must be recomputed fresh The feature must recompute or re-read fresh: - API key availability - binary/runtime availability - current preferred auth mode from config - login pending state - logout in progress state ### What must never be carried forward Do not carry forward: - pending login state - failed login state - logout in progress - rate limit snapshots beyond their own TTL - a degraded state as if it were a successful state ### Freshness expiration rule Once the freshness window expires: - the feature may still show `degraded` - but it must stop treating stale managed-account evidence as sufficient for launchability ### Post-logout rule After explicit logout success: - clear the last-known-good managed-account snapshot immediately - do not allow degraded reads to resurrect the old account state ## Feature Data Contracts ## DTOs ### `CodexConnectionSnapshotDto` Fields: - `state` - `preferredAuthMode` - `effectiveAuthMode` - `binaryAvailable` - `requiresOpenaiAuth` - `managedAccount` - `apiKey` - `launchReadiness` - `degradedReason` - `login` - `observedAt` ### `CodexManagedAccountDto` Fields: - `type: "chatgpt" | "apiKey" | null` - `email?: string | null` - `planType?: "free" | "go" | "plus" | "pro" | "team" | "business" | "enterprise" | "edu" | "unknown" | null` Important note: - we are not planning to use app-server `apiKey` login mode, but the DTO should still model it defensively because the protocol supports it ### `CodexApiKeyAvailabilityDto` Fields: - `available: boolean` - `source: "stored" | "environment" | null` - `label?: string | null` ### `CodexLaunchReadinessDto` Fields: - `state` - `message` - `effectiveAuthMode` ### `CodexLoginStateDto` Fields: - `state: "idle" | "starting" | "awaiting_browser" | "pending" | "completed" | "cancelled" | "failed"` - `loginId?: string | null` - `message?: string | null` Sensitive-field rule: - `loginId` is process-lifecycle metadata and should not be rendered to the user - renderer-facing contracts may omit `loginId` entirely if cancellation and status updates can be driven by main-owned session state - raw `authUrl` must never appear in renderer-facing DTOs ### `CodexRateLimitsDto` Fields: - `rateLimits` - `rateLimitsByLimitId?` - `planType?` - `observedAt` ## Event contract Recommended event union: - `snapshot-updated` - `login-state-changed` - `rate-limits-updated` - `degraded` These should be emitted over one feature event channel and consumed by renderer hooks. ## DTO And Event Shape Examples The plan should include concrete examples so that main, preload, and renderer do not each invent their own interpretation. ### Example `CodexConnectionSnapshotDto` ```json { "state": "both_available", "preferredAuthMode": "auto", "effectiveAuthMode": "chatgpt", "binaryAvailable": true, "requiresOpenaiAuth": true, "managedAccount": { "type": "chatgpt", "email": "user@example.com", "planType": "pro" }, "apiKey": { "available": true, "source": "stored", "label": "Stored in app" }, "launchReadiness": { "state": "ready_both", "message": "ChatGPT account connected - API key also available", "effectiveAuthMode": "chatgpt" }, "degradedReason": null, "login": { "state": "idle", "loginId": null, "message": null }, "observedAt": 1776640000000 } ``` ### Example degraded snapshot ```json { "state": "degraded", "preferredAuthMode": "auto", "effectiveAuthMode": "chatgpt", "binaryAvailable": true, "requiresOpenaiAuth": true, "managedAccount": { "type": "chatgpt", "email": "user@example.com", "planType": "pro" }, "apiKey": { "available": false, "source": null, "label": null }, "launchReadiness": { "state": "warning_degraded_but_launchable", "message": "ChatGPT account detected - verification degraded", "effectiveAuthMode": "chatgpt" }, "degradedReason": "app-server-timeout", "login": { "state": "idle", "loginId": null, "message": null }, "observedAt": 1776640005000 } ``` ### Example `login-state-changed` event ```json { "type": "login-state-changed", "payload": { "state": "pending", "message": "Waiting for ChatGPT browser login to complete" }, "observedAt": 1776640002000 } ``` ### Example `snapshot-updated` event ```json { "type": "snapshot-updated", "payload": { "state": "managed_account_connected", "preferredAuthMode": "chatgpt", "effectiveAuthMode": "chatgpt" }, "observedAt": 1776640008000 } ``` Contract rule: - event payloads may be smaller than full DTOs - snapshot read methods must still return the full DTO - renderer must not assume an event payload is a complete replacement snapshot unless the contract explicitly says so Sensitive-field containment rule: - `authUrl` must never be emitted over the feature event channel - incremental events should not include full account email unless a full snapshot read is actually required for UI rendering - `loginId` should stay main-owned unless there is a concrete renderer need that cannot be solved by process-owned cancel/status APIs ## Error Normalization Matrix The feature should normalize raw transport/process failures into stable categories so renderer copy and metrics stay coherent. | Raw failure family | Normalized category | Typical feature impact | UI treatment | | --- | --- | --- | --- | | app-server initialize timeout | `app-server-timeout` | degraded snapshot or failed login start | degraded / retryable message | | app-server process spawn failure | `app-server-unavailable` | degraded snapshot or hard login failure | binary/runtime dependent messaging | | app-server initialize succeeds but required stable account surface is unavailable | `app-server-incompatible` | feature hidden/locked or hard login failure | update-runtime / incompatible-runtime messaging | | login returned unsafe or unsupported browser URL | `unsafe-auth-url` | login fails before browser open | explicit security-oriented error, no open attempt | | browser open failure | `browser-open-failed` | login failed | explicit action error | | login cancelled by user | `login-cancelled` | login state settles to cancelled | non-destructive informational state | | login completed with error | `login-failed` | login failed, snapshot refresh follows | explicit error | | logout RPC failure | `logout-failed` | logout stays unresolved, snapshot preserved | explicit error | | admin-managed workspace or login policy rejects current account | `workspace-restricted` | login blocked or account cleared by Codex policy | policy-specific guidance, not generic auth-missing | | app restarted or shut down while login was pending | `login-session-lost` | pending login abandoned, fresh snapshot required on next startup | informational recovery message, settle to idle | | rate-limits read failure | `rate-limits-unavailable` | rate-limit panel degraded only | non-blocking warning | | stale result received after newer state | `stale-result-ignored` | no user-visible state change | debug-level logging only | Normalization rule: - renderer copy should key off normalized categories - raw stderr / transport text may be attached for diagnostics, but should not drive UX wording ## Event Ordering And Delivery Rules These rules matter because most user-visible bugs in this feature will come from correct data arriving in the wrong order. ### Preferred event ordering For login success: 1. `login-state-changed: starting` 2. `login-state-changed: awaiting_browser` 3. `login-state-changed: pending` 4. app-server `account/login/completed success=true` 5. `login-state-changed: completed` 6. forced snapshot refresh 7. `snapshot-updated` 8. optional `rate-limits-updated` after explicit or lazy read 9. `login-state-changed: idle` once settled For logout success: 1. local logout action enters pending state 2. `account/logout` 3. clear last-known-good managed-account snapshot 4. forced snapshot refresh 5. `snapshot-updated` 6. local logout pending state clears ### Delivery rule Feature event delivery should be best-effort and additive: - missing one event must not make the renderer permanently stale - the renderer must always be able to recover by reading a fresh snapshot ### Idempotency rule Renderer and main-side subscribers should tolerate: - duplicate `login-state-changed` - duplicate `snapshot-updated` - late degraded events that are older than the currently rendered snapshot ### Staleness rejection rule If an incoming event or refresh result is older than the settled snapshot already in memory: - ignore it - log a low-level diagnostic if useful Do not let older results reanimate older UI state. ## Cross-Window And Subscriber Coherence Rules The main process must remain the single owner of mutable Codex account state. ### Main-process ownership rule The following state must live in one main-process feature instance, not in renderer-local stores: - latest settled snapshot - last-known-good managed account snapshot - active login session state - rate-limit cache - compatibility verdict for the installed app-server seam ### Renderer subscription rule Any renderer may subscribe late, unsubscribe early, or restart independently. Therefore: - late subscribers must bootstrap from `getSnapshot()` and not rely on having seen past events - feature events are accelerators, not the only source of truth - one renderer closing must not cancel a login session started by another renderer ### Broadcast rule If multiple renderer surfaces are open at once: - all should receive the same normalized feature events from the same main-process owner - no renderer should start a second login flow just because it missed an earlier local UI state ### Shutdown rule If the last renderer unsubscribes while a login is pending: - the main-process feature may keep the login session alive until: - completion - explicit cancel - timeout - app shutdown This avoids coupling correctness to whichever window happened to open the flow. ## App Restart, Crash, And Pending Login Recovery Policy Login session state is explicitly process-owned, not durable product state. ### Startup recovery rule On app startup: - do not restore a previously pending login from persisted config or renderer state - initialize login state as `idle` - perform a fresh snapshot read to determine the actual steady state ### Shutdown rule On app shutdown while login is pending: - do not block app quit waiting for remote login completion - a best-effort `account/login/cancel` is optional, but correctness must not depend on it - local session state should settle as lost/abandoned for diagnostics only ### Crash/reload rule If the renderer reloads or the app restarts during login: - the next session should not show a phantom permanently pending state - the feature should converge via fresh `account/read`, not by trying to resume an old `loginId` ### Persistence rule Do not persist: - pending login state - `loginId` - `authUrl` Persist only durable user intent: - preferred auth mode ## Operation Serialization And Race Resolution Policy The feature must treat auth mutations as serialized control-plane operations, not as unrelated UI button handlers. ### Serialization rule Serialize these operations through one main-process account-operation gate: - start login - cancel login - logout - explicit auth-recovery refresh that escalates to `refreshToken = true` Passive reads may coalesce, but mutating operations must not run in parallel. ### Race resolution rule If mutation intent and observed remote completion disagree, final truth comes from the freshest successful post-mutation snapshot, not from the earlier button click alone. ### Required race outcomes 1. cancel requested, then late `login/completed success=true` arrives: - do a forced snapshot refresh - if the snapshot shows a connected managed account, show connected state - do not force logout just to honor the earlier cancel intent 2. logout requested during pending login: - best-effort cancel login first if needed - then run logout - final logged-out truth must win over any stale pre-logout account evidence 3. stale read started before logout settles after logout: - ignore the stale read result - never resurrect the old managed account from that stale result ### Preference-change rule Changing preferred auth mode during a pending login affects future launch choice only. It must not: - silently cancel an in-flight login unless product explicitly chooses that UX later - rewrite the meaning of a login session that already started under the prior preference ## Protocol Assumptions We Intentionally Avoid Relying On The feature should stay robust even if some secondary protocol details vary. Do not rely on: - `account/updated` always arriving before or after `account/login/completed` - rate-limit notifications being delivered during every app lifecycle - undocumented auth variants being available in every Codex build - raw CLI reference pages being the only authoritative source for auth override details - exact raw transport error text remaining stable enough for renderer copy Instead, rely on: - explicit snapshot refresh for steady-state truth - normalized error categories - narrow, documented auth variants for first-wave behavior ## IPC channels Recommended channels: - `CODEX_ACCOUNT_GET_SNAPSHOT` - `CODEX_ACCOUNT_REFRESH_SNAPSHOT` - `CODEX_ACCOUNT_START_CHATGPT_LOGIN` - `CODEX_ACCOUNT_CANCEL_LOGIN` - `CODEX_ACCOUNT_LOGOUT` - `CODEX_ACCOUNT_GET_RATE_LIMITS` - `CODEX_ACCOUNT_EVENT` ## IPC Request / Response Matrix This matrix keeps the cross-process contract explicit and prevents renderer/main drift. | Channel | Request shape | Response shape | Side effects | Cache interaction | Failure shape | | --- | --- | --- | --- | --- | --- | | `CODEX_ACCOUNT_GET_SNAPSHOT` | `{ forceFresh?: boolean }` or no payload | `CodexConnectionSnapshotDto` | none | may serve cached snapshot unless `forceFresh` | returns rejected IPC promise with normalized error message | | `CODEX_ACCOUNT_REFRESH_SNAPSHOT` | no payload | `CodexConnectionSnapshotDto` | forces control-plane refresh | bypasses normal snapshot TTL | returns rejected IPC promise with normalized error message | | `CODEX_ACCOUNT_START_CHATGPT_LOGIN` | no payload | `CodexLoginStateDto` | starts login session, may open browser | invalidates snapshot cache on state transitions | returns failed login state or rejected IPC promise for hard failures | | `CODEX_ACCOUNT_CANCEL_LOGIN` | no payload | `CodexLoginStateDto` | cancels active login if present | no steady-state cache effect except follow-up refresh | safe no-op if no active login | | `CODEX_ACCOUNT_LOGOUT` | no payload | `CodexConnectionSnapshotDto` | logs out managed account and refreshes snapshot | clears last-known-good managed-account snapshot | rejected IPC promise or error-bearing snapshot depending on final implementation choice | | `CODEX_ACCOUNT_GET_RATE_LIMITS` | `{ forceFresh?: boolean }` or no payload | `CodexRateLimitsDto \| null` | none | may serve rate-limit cache unless `forceFresh` | returns `null` or rejected IPC promise depending on error-handling contract | | `CODEX_ACCOUNT_EVENT` | subscription only | `CodexAccountEventDto` | none | n/a | best-effort delivery only | Contract rule: - all IPC errors should be normalized into stable, user-safe messages - renderer code must not depend on raw transport/process error text ## Electron API integration Extend: - `src/shared/types/api.ts` - `src/preload/index.ts` Pattern should match existing feature slices like `recent-projects`. ## Preload And Renderer API Shape The renderer-facing API should be explicit so we do not leak main-process internals into UI code. Recommended preload-facing contract: ```ts export interface CodexAccountElectronApi { getSnapshot: (options?: { forceFresh?: boolean }) => Promise; refreshSnapshot: () => Promise; startChatgptLogin: () => Promise; cancelLogin: () => Promise; logout: () => Promise; getRateLimits: (options?: { forceFresh?: boolean }) => Promise; onEvent: (callback: (event: CodexAccountEventDto) => void) => () => void; } ``` Integration rule: - this contract belongs under `src/features/codex-account/contracts` - `src/preload/index.ts` should only bridge it - renderer hooks should consume this contract through the app API abstraction, not directly through ad hoc `window.electronAPI` calls spread across components ## Account Control-Plane Flows ## App-Server Method Matrix This matrix documents how the feature is expected to use the official app-server surface. | Method / notification | Used in phase | Typical caller | Input | Expected output | Notes | | --- | --- | --- | --- | --- | --- | | `account/read` | B+ | snapshot use cases | `{ refreshToken?: boolean }` | current account plus `requiresOpenaiAuth` | passive reads should default `refreshToken` to `false` | | `account/login/start` with `type: "chatgpt"` | E+ | login use case | no extra payload | `loginId` plus `authUrl` | browser flow is first-class | | `account/login/cancel` | E+ | cancel use case / session manager | active `loginId` | success or no-op | safe to call only when login is active | | `account/logout` | E+ | logout use case | none | logout acknowledgement | should be followed by forced snapshot refresh | | `account/rateLimits/read` | F+ | rate-limit use case | none or method-specific default params | plan/rate-limit snapshot | should stay lazy by default | | `account/updated` | E+ | login session manager / event bridge | notification only | auth mode plus plan changes | may arrive outside explicit reads | | `account/login/completed` | E+ | login session manager | notification only | success or error for a specific `loginId` | must drive pending-state settlement | | `account/rateLimits/updated` | F+ | optional rate-limit subscription handling | notification only | updated rate-limit view | should not be required for base snapshot correctness | Usage rule: - `account/read` is the canonical steady-state read path - notifications are accelerators for freshness, not replacements for a recoverable read model ## `refreshToken` Usage Policy Official app-server docs and local schema expose `account/read { refreshToken?: boolean }`. This flag is powerful enough that the plan should constrain it explicitly. ### Default rule - passive background reads use `refreshToken = false` - normal explicit refresh also starts with `refreshToken = false` ### Escalation rule Allow a one-time `refreshToken = true` read only when there is a concrete auth-staleness reason, for example: - a just-completed login has not converged after the first normal snapshot read - explicit user recovery action after an auth-related degraded state ### Forbidden rule Do not: - set `refreshToken = true` on every poll - loop repeated token-refresh reads in the background - use token refresh as a substitute for the normal snapshot model ### Operational reason - overusing token refresh increases latency and creates another path to false logout or confusing transient state ## App-Server Compatibility Gate The feature must not equate "binary exists" with "account seam is supported". ### Stable-surface rule First-wave `codex-account` should initialize app-server with: - `experimentalApi: false` - only the notification subscriptions it actually needs Do not opt into experimental API just to make the first wave easier. ### Required handshake contract Before the feature reports the managed-account seam as supported, it must prove: 1. `codex app-server` starts 2. `initialize` succeeds on the stable surface 3. the initialize response yields diagnostics we can record: - `codexHome` - `platformFamily` - `platformOs` 4. required stable methods behave as expected: - `account/read` - `account/login/start` - `account/login/cancel` - `account/logout` - `account/rateLimits/read` ### Compatibility verdict rule If initialize works but one of the required stable account methods is absent, rejected as experimental-only, or otherwise incompatible with the expected shape: - classify the feature state as `app-server-incompatible` - do not classify it as generic missing auth - do not offer misleading login or subscription controls ### Versioning rule Prefer capability/protocol evidence over string-parsed `codex --version`. Semver may still be logged for diagnostics, but: - semver alone must not unlock the feature - semver alone must not disable the feature when the stable handshake succeeds ## Flow 1 - autodetect existing account ```mermaid sequenceDiagram participant UI as Renderer Hook participant IPC as Feature IPC participant UC as Get Snapshot Use Case participant APP as App Server Client participant KEY as API Key Adapter UI->>IPC: getCodexConnectionSnapshot() IPC->>UC: execute() UC->>APP: account/read UC->>KEY: getApiKeyAvailability() APP-->>UC: managed account state KEY-->>UC: api key availability UC-->>IPC: merged snapshot IPC-->>UI: snapshot dto ``` ## Flow 2 - start ChatGPT login ```mermaid sequenceDiagram participant UI as Renderer participant IPC as Feature IPC participant UC as Start Login Use Case participant LOGIN as Login Session Manager participant APP as App Server participant BROWSER as Browser Launcher UI->>IPC: startCodexChatgptLogin() IPC->>UC: execute() UC->>LOGIN: acquire or create login session LOGIN->>APP: account/login/start(type=chatgpt) APP-->>LOGIN: authUrl + loginId LOGIN->>BROWSER: openExternal(authUrl) LOGIN-->>IPC: login pending IPC-->>UI: login state changed APP-->>LOGIN: account/login/completed LOGIN-->>IPC: snapshot refresh + login completed event IPC-->>UI: snapshot updated ``` ## Flow 3 - launch Codex ```mermaid sequenceDiagram participant SHELL as Runtime Shell participant FACADE as Codex Account Feature participant READY as Launch Readiness UC participant ENV as Provider Env Builder participant EXEC as codex exec SHELL->>FACADE: evaluateLaunchReadiness() FACADE->>READY: execute() READY-->>FACADE: readiness + effectiveAuthMode + env policy FACADE-->>SHELL: launch policy SHELL->>ENV: build env using policy ENV-->>SHELL: sanitized/injected env SHELL->>EXEC: launch codex exec ``` ## Login State Machine ```mermaid stateDiagram-v2 [*] --> idle idle --> starting: start login starting --> awaiting_browser: authUrl received awaiting_browser --> pending: browser opened pending --> completed: account/login/completed success=true pending --> failed: account/login/completed success=false pending --> cancelled: account/login/cancel awaiting_browser --> failed: browser open failed starting --> failed: start request failed completed --> idle: refresh settled cancelled --> idle: refresh settled failed --> idle: reset ``` ## Subscription Lifecycle Semantics This section describes the intended steady-state lifecycle of a managed ChatGPT-backed Codex subscription as the app should understand it. ### Lifecycle phases 1. no managed account detected 2. login initiated 3. browser auth pending 4. managed account connected 5. temporarily degraded verification 6. explicit logout ### Important semantic rules - `managed account connected` means the control plane has positive evidence of a ChatGPT-backed account - `degraded` does not mean disconnected - explicit logout is stronger than cached prior evidence and must clear it immediately - API key availability may coexist with any lifecycle phase except `runtime_missing` ### User-visible implication The UI should present the managed account as: - connected - pending - degraded - disconnected and should not collapse these into one binary `authenticated` flag for Codex. ## Main-Side Use Cases ### `GetCodexConnectionSnapshotUseCase` Responsibilities: - get a cached or fresh merged snapshot - merge managed account and API key availability - derive effective auth mode and launch readiness Must not: - open browser - mutate config ### `RefreshCodexConnectionSnapshotUseCase` Responsibilities: - force a fresh read from app-server - optionally request proactive token refresh only on explicit user action Important nuance: - do **not** set `refreshToken = true` on every passive read - reserve that for explicit manual refresh or post-login reconciliation ### `StartCodexChatgptLoginUseCase` Responsibilities: - ensure a single in-flight login - start login via app-server - open browser - update login state Must handle: - duplicate click while login already pending - browser open failure - login timeout First implementation decision: - browser flow via `type: "chatgpt"` is in scope - device code flow via `type: "chatgptDeviceCode"` is explicitly deferred unless we discover a real Electron/browser-launch blocker Reason: - browser flow matches the prior UX expectation more closely - device-code support is valuable, but it is not required to restore the intended desktop UX ### `CancelCodexLoginUseCase` Responsibilities: - cancel active login if any - cleanly tear down pending session ### `LogoutCodexManagedAccountUseCase` Responsibilities: - perform app-server `account/logout` - refresh snapshot ### `ReadCodexRateLimitsUseCase` Responsibilities: - load rate limits lazily - avoid blocking basic connection UI ### `EvaluateCodexLaunchReadinessUseCase` Responsibilities: - compute launch policy from feature truth - return: - readiness state - effective auth mode - env mutation instructions - user-facing advisory This use case must become the shell's single source of truth for Codex launch auth policy. ## Main Infrastructure Design ## `CodexAccountEnvBuilder` Purpose: - build consistent env for app-server account sessions - build deterministic env mutation instructions for execution Inputs: - resolved shell env - binary path - selected auth mode - API key value or absence Outputs: - account-session env - exec-session env policy This module exists because generic `buildProviderAwareCliEnv()` currently knows only the old Codex API-key-only world. ## `CodexAccountAppServerClient` Purpose: - short-lived request client for: - `account/read` - `account/logout` - `account/rateLimits/read` Behavior: - request-scoped sessions only - initialize and dispose per request ## `CodexLoginSessionManager` Purpose: - own one long-lived login session while login is pending Responsibilities: - start login - observe notifications - cancel login - timeout pending login - emit feature events Important rule: - there can be at most one active login session at a time ## Module Responsibility Matrix This table is the SRP-oriented version of the implementation design. | Module | Owns | Must not own | Typical collaborators | | --- | --- | --- | --- | | `CodexAccountAppServerClient` | request-scoped app-server RPC for account/read, logout, rate limits | login session lifecycle, renderer events, config | session factory, logger | | `CodexLoginSessionManager` | long-lived login session, login notifications, timeout, cancel | generic account/read cache, API key storage, shell copy | app-server transport, browser launcher, logger | | `CodexAccountEnvBuilder` | auth-store env normalization, auth-sensitive env mutation policy | launch decision semantics, config migration | shell env port, binary resolver port | | `GetCodexConnectionSnapshotUseCase` | merge snapshot truth and caching policy | browser opening, low-level process logic | managed account source, api key source, cache, clock | | `EvaluateCodexLaunchReadinessUseCase` | effective auth mode selection and readiness semantics | actual child-process spawning | snapshot/domain models | | `CodexConnectionCoordinator` | shell-facing launch integration and env assembly handoff | account lifecycle, renderer subscriptions | feature facade, provider connection service | | presenter adapters | stable DTO/event shaping | domain policy changes | use cases, contracts | | renderer hooks | subscription orchestration and action wiring | business truth invention | preload API, view-model adapters | | feature UI components | rendering | transport, config mutation, process logic | hooks, view models | Review rule: - if a new change makes one row start owning another row's responsibilities, stop and split the concern before continuing ## Session And Timeout Policy ### Short-lived read sessions Use for: - account/read - rateLimits/read - logout Recommended timeouts: - initialize timeout: aligned with existing app-server defaults - request timeout: short and bounded ### Long-lived login session Use for: - start login - wait for `account/login/completed` Recommended timeout: - hard max pending duration around `10 minutes` Reason: - long enough for browser auth - short enough to avoid zombie sessions ## App-Server Session Topology Rule The feature should make session ownership explicit so connection-scoped notifications do not leak between concerns. ### Topology - passive reads: - short-lived dedicated app-server sessions - rate-limits reads: - short-lived dedicated app-server sessions or reused read-session helper, but not the live login session - login flow: - one dedicated long-lived session that owns: - `account/login/start` - login notifications - optional `account/login/cancel` ### No-pooling rule Do not multiplex in the first wave: - `recent-projects` and `codex-account` over one shared live app-server child - login notifications and passive reads over one generic pooled session ### Cleanup rule When a feature-owned app-server session is disposed: - kill the child deterministically - reject outstanding requests - ignore any late results or notifications from that disposed session generation This keeps process cleanup and notification ownership unambiguous. ### Notification suppression policy For read sessions: - suppress noisy thread notifications For login session: - do **not** suppress: - `account/login/completed` - `account/updated` ## Timeout Defaults Table These defaults should remain centralized so different callers do not invent incompatible timing. | Interaction | Recommended default | Why | | --- | --- | --- | | app-server initialize for read sessions | align with existing shared app-server defaults | keeps transport consistent with `recent-projects` | | `account/read` request timeout | short and bounded | passive reads should fail fast into degraded state | | `account/logout` request timeout | short and bounded | logout should resolve or fail clearly | | `account/rateLimits/read` timeout | short-medium | secondary UI should not hang the page | | login pending max duration | around `10 minutes` | enough for browser auth, short enough to avoid zombie state | | snapshot cache TTL | around `3-10 seconds` | enough dedupe without stale-feeling UI | | rate-limits cache TTL | around `30-60 seconds` | secondary UI can be less fresh | | freshness window for last-known-good managed account | around `60 seconds` | balances resilience and honesty | Consistency rule: - do not hardcode these values independently in multiple modules - centralize them in feature-local configuration/constants or shared transport defaults where appropriate ## Retry And Backoff Policy Retries are one of the easiest ways to accidentally hide truth or create duplicate state transitions. This feature should be conservative. ### What may retry automatically - passive `account/read` refresh after a transient initialization or timeout failure Recommended default: - at most one immediate retry for passive reads - only when the failure is clearly transport-level ### What should not auto-retry - login start - logout - cancel login - manual refresh button actions - any request that already has a user-visible action outcome Why: - auto-retrying user actions can create duplicate browser flows, duplicate state transitions, or surprising side effects ### Backoff rule If passive background refresh keeps failing: - do not spin - let the feature surface `degraded` - wait for next normal refresh trigger or explicit user action ### Timeout handling rule Timeout must be surfaced as a first-class degraded reason category, not collapsed into generic "not connected". ## Caching And Refresh Policy ### Snapshot cache Recommended: - in-memory cache in main feature - small TTL around `3-10 seconds` - single-flight refresh collapse Reason: - dashboard banner and settings dialog can ask for the same snapshot nearly simultaneously ### Rate-limits cache Recommended: - separate cache - longer TTL around `30-60 seconds` Reason: - rate limits are secondary UI, not critical hot-path state ### Post-action invalidation After: - login success - logout success - explicit refresh invalidate the snapshot cache immediately. ## Shell Integration Plan This section makes the integration concrete. ## Main process composition Add feature creation in: - `src/main/index.ts` Pattern: - create the feature alongside `recent-projects` - register its IPC handlers after feature construction Later, if browser-mode support is desired: - wire the feature into `src/main/standalone.ts` - add HTTP adapter endpoints ## Preload integration Add to: - `src/preload/index.ts` Pattern: - same as `recent-projects` - spread `createCodexAccountBridge()` into `window.electronAPI` ## Shared API type integration Extend: - `src/shared/types/api.ts` with a new feature API contract interface. ## Renderer integration Shell components that must stop owning Codex business logic: - `ProviderRuntimeSettingsDialog` - `CliStatusBanner` - `CliStatusSection` - `providerConnectionUi.ts` How they should change: 1. `ProviderRuntimeSettingsDialog` - for provider `codex`, render `CodexAccountConnectionPanel` - stop hardcoding Codex as API-key-only 2. `CliStatusBanner` - stop using terminal modal login/logout for Codex - use feature hook actions 3. `CliStatusSection` - same as banner 4. `providerConnectionUi.ts` - stop flattening Codex auth summary - use feature adapter output for Codex-specific text ## Legacy UX Parity Policy The user requirement here is not just "make auth work". It is also "restore the good legacy Codex subscription UX while keeping the new native runtime". That means we should reuse the current shell surfaces and preserve their visual grammar, instead of inventing a new Codex settings screen. ### Required UX shape The feature should plug into the existing surfaces: - provider manage dialog - dashboard CLI banner - settings CLI section It should not introduce: - a separate standalone Codex settings page - a second disconnected login modal system - a renderer-only fake status card ### Required visible information For Codex, the composed UI should be able to show: - current preferred auth mode - managed account connected or not - account email when available - plan type when available - API key also available or not - effective launch mode in auto - pending login / cancelling / logout states - degraded-but-still-launchable states ### Required action set For Codex, the composed UI should expose: - connect ChatGPT account - cancel login while pending - disconnect managed account - choose preferred auth mode - manage API key without implying it is the only path - refresh account state ### Copy policy When Codex is managed-account-backed: - do not flatten everything to "Connected via API key" - do not label the primary action as `Configure API key` - do not use Anthropic-specific `OAuth` wording Preferred wording family: - `ChatGPT account` - `Codex subscription` - `Plan` - `API key also available` - `Auto - prefer ChatGPT account` ### Visual ownership rule The feature owns Codex-specific content blocks. The shell owns: - container cards - section framing - generic button spacing/layout primitives - provider ordering This preserves legacy familiarity without duplicating layout systems. ## Surface-By-Surface UI Contract Each visible shell surface should have a clear responsibility so the same state is not explained in three conflicting ways. ### `ProviderRuntimeSettingsDialog` Purpose: - authoritative management surface for Codex auth preference and account state Must show: - preferred auth mode selector - managed account summary - API key secondary availability - login / cancel / logout actions - degraded state explanation - rate-limit section when requested or expanded Must not: - pretend to be a generic provider card when provider is `codex` ### `CliStatusBanner` Purpose: - concise dashboard summary and quick action entry point Must show: - one-line Codex status summary derived from the feature - manage/open action into the richer settings surface - degraded warning when appropriate Must not: - become the full account management UI ### `CliStatusSection` Purpose: - settings-level operational summary for the installed runtime Must show: - consistent Codex account summary - launch-relevant status - path into full manage dialog Must not: - use different wording than the manage dialog for the same auth truth ### Shared UI consistency rule For the same underlying snapshot: - headline wording - effective auth mode wording - degraded wording - connect/disconnect affordances must all stay semantically consistent across surfaces, even if the amount of detail differs. ## What remains shell-owned - generic provider card structure - generic runtime/backend status - generic model availability presentation ## What becomes feature-owned - Codex account summary copy - Codex connect/disconnect actions - Codex login pending UI - Codex rate-limit presentation - Codex auth-mode selection UI ## Renderer Hook Composition Contract To keep renderer code aligned with the feature standard, hooks and adapters should have explicit roles. ### `useCodexAccount` Responsibilities: - fetch and subscribe to the current snapshot - expose refresh action - expose derived loading/error/degraded flags for feature UI Must not: - open browser directly - compute shell layout copy inline ### `useCodexLoginFlow` Responsibilities: - expose login, cancel, and logout actions - surface pending action state and latest action error Must not: - own snapshot caching - own rate-limit fetching ### `useCodexRateLimits` Responsibilities: - fetch rate limits lazily - respect dedicated rate-limit TTL and pending state Must not: - block base snapshot rendering ### `codexAccountViewModel` / `codexProviderShellAdapter` Responsibilities: - merge feature DTOs with generic shell provider status into stable view models - keep wording and badge semantics consistent across surfaces Must not: - call transport directly - mutate app config ### UI component rule Feature UI components should be as close to pure renderers as possible: - inputs in - callbacks out This keeps renderer complexity low and makes snapshot-state regression tests much easier. ## Browser-mode policy Initial implementation recommendation: - Electron / preload path is the first-class path - browser-mode support is explicitly deferred unless product requires it immediately If browser mode is visible: - Codex account feature should degrade honestly as unsupported or unavailable - do not silently attempt local app-server control through browser mode without explicit HTTP support This keeps the architecture clean and avoids half-working local machine assumptions in browser sessions. ## Browser Auth URL Handling Policy The feature should treat login URLs as sensitive, short-lived control-plane data. ### Open rule For Codex ChatGPT login: - open the URL from main process only - validate the URL before opening - require `https:` scheme - reject `http:`, `mailto:`, custom schemes, or malformed URLs for this feature-specific path ### Trust rule Do not hardcode a hostname allowlist in the first wave unless official docs start guaranteeing a stable host set. Instead: - trust app-server as the source of the URL - enforce `https` scheme - avoid logging the full URL - record only derived diagnostics when needed, such as scheme/hostname ### IPC rule - renderer asks to start login - main starts login and opens the validated URL - renderer never receives the raw `authUrl` ### Failure rule If URL validation fails: - classify as `unsafe-auth-url` - fail login cleanly - do not attempt browser open ## Runtime Integration Plan This is where the feature touches existing runtime services. ## Existing problem `providerAwareCliEnv.ts` and `ProviderConnectionService.ts` currently encode the rule: - Codex readiness requires API key presence That must change. ## Recommended integration pattern Introduce a small runtime-side coordinator: - `src/main/services/runtime/CodexConnectionCoordinator.ts` Responsibilities: - ask the feature for launch readiness - ask `ProviderConnectionService` for API key value resolution when needed - apply auth-mode-specific env mutation policy Why a coordinator is better than stuffing more into `ProviderConnectionService`: - keeps provider-generic code smaller - avoids turning `ProviderConnectionService` into a God object - keeps Codex feature policy close to the feature seam ## Responsibilities after the coordinator exists ### `ProviderConnectionService` keeps responsibility for: - app-owned API key discovery - app-owned API key injection primitives - Anthropic-specific connection mode handling ### `CodexConnectionCoordinator` owns: - choosing ChatGPT vs API key for Codex launch - deciding which env vars must be sanitized - deciding which `forced_login_method` override to pass ### `providerAwareCliEnv.ts` becomes: - provider-generic env assembly plus delegation to Codex coordinator when provider is `codex` ### `ClaudeMultimodelBridgeService.ts` becomes: - generic runtime status reader - plus additive merge of Codex account snapshot for Codex-specific presentation ## Detailed Touch Points Likely first-wave touch points: - `src/main/index.ts` - `src/preload/index.ts` - `src/shared/types/api.ts` - `src/main/services/infrastructure/ConfigManager.ts` - `src/main/ipc/configValidation.ts` - `src/shared/types/notifications.ts` - `src/main/services/runtime/providerAwareCliEnv.ts` - `src/main/services/runtime/ProviderConnectionService.ts` - `src/main/services/runtime/ClaudeMultimodelBridgeService.ts` - `src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx` - `src/renderer/components/runtime/providerConnectionUi.ts` - `src/renderer/components/dashboard/CliStatusBanner.tsx` - `src/renderer/components/settings/sections/CliStatusSection.tsx` Important design rule: - shell files should mostly lose Codex-specific conditional logic, not gain more of it ## File-Level Implementation Map This section translates the architecture into concrete repo edits so the work stays transparent. ### New feature files Expected first-wave additions: - `src/features/codex-account/contracts/api.ts` - `src/features/codex-account/contracts/channels.ts` - `src/features/codex-account/contracts/dto.ts` - `src/features/codex-account/contracts/events.ts` - `src/features/codex-account/contracts/index.ts` - `src/features/codex-account/core/domain/*` - `src/features/codex-account/core/application/ports/*` - `src/features/codex-account/core/application/use-cases/*` - `src/features/codex-account/main/composition/createCodexAccountFeature.ts` - `src/features/codex-account/main/adapters/input/ipc/registerCodexAccountIpc.ts` - `src/features/codex-account/main/adapters/output/presenters/*` - `src/features/codex-account/main/adapters/output/runtime/*` - `src/features/codex-account/main/infrastructure/cache/*` - `src/features/codex-account/main/infrastructure/codex/*` - `src/features/codex-account/preload/createCodexAccountBridge.ts` - `src/features/codex-account/preload/index.ts` - `src/features/codex-account/renderer/index.ts` - `src/features/codex-account/renderer/adapters/*` - `src/features/codex-account/renderer/hooks/*` - `src/features/codex-account/renderer/ui/*` ### Existing files that should mostly gain composition hooks, not deep new business logic #### `src/main/index.ts` Should: - instantiate the feature - register IPC - expose a small facade to shell services Should not: - implement account/read logic inline - manage login state inline #### `src/preload/index.ts` Should: - merge the feature bridge into `window.electronAPI` Should not: - contain auth logic - transform account domain state into shell copy #### `src/main/services/infrastructure/ConfigManager.ts` Should: - add persisted `providerConnections.codex.preferredAuthMode` - normalize stale legacy Codex connection values Should not: - resolve runtime effective auth mode #### `src/main/ipc/configValidation.ts` Should: - accept only the new normalized Codex connection field - reject new unknown fields after migration normalization Should not: - infer defaults that belong to `ConfigManager` #### `src/main/services/runtime/ProviderConnectionService.ts` Should: - remain owner of app API key storage lookup and injection primitives Should not: - remain final authority for Codex launch readiness - own ChatGPT managed-account policy #### `src/main/services/runtime/providerAwareCliEnv.ts` Should: - delegate Codex-specific env policy to the coordinator / feature seam Should not: - continue hardcoding API-key-only Codex logic #### `src/main/services/runtime/ClaudeMultimodelBridgeService.ts` Should: - keep generic provider/runtime status probing - optionally compose Codex account snapshot into Codex-facing presentation Should not: - become the owner of Codex account lifecycle #### `src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx` Should: - host the feature-owned Codex panel Should not: - directly own Codex login flow logic - directly derive Codex copy from generic provider flags alone #### `src/renderer/components/runtime/providerConnectionUi.ts` Should: - become thinner for Codex - consume feature adapters for Codex-specific labels Should not: - remain the place where Codex account semantics are invented #### `src/renderer/components/dashboard/CliStatusBanner.tsx` #### `src/renderer/components/settings/sections/CliStatusSection.tsx` Should: - call feature actions / hooks - render feature-composed Codex status segments Should not: - keep normal Codex login/logout on terminal modal commands once the feature is complete ## Failure Modes And Safety Policy ### Failure mode: no Codex binary Behavior: - state becomes `runtime_missing` - login actions disabled - connection panel explains binary missing ### Failure mode: app-server initialize failure Behavior: - managed-account state becomes degraded - API key availability remains visible - do not auto-mark user as logged out - do not hard-stop launch if execution could still work Additional rule: - if the last successful snapshot is still within the acceptable freshness window, prefer showing `degraded` over collapsing to `not_connected` ### Failure mode: account read timeout Behavior: - same as degraded - last good snapshot may be reused briefly if not stale beyond TTL ### Failure mode: login start failure Behavior: - `login.state = failed` - snapshot remains refreshable ### Failure mode: unsafe auth URL Behavior: - `login.state = failed` - do not attempt browser open - surface a security-oriented error category rather than a generic browser failure ### Failure mode: browser open failure Behavior: - `login.state = failed` - no implicit retry loop - preserve returned login metadata in memory long enough to support explicit retry or diagnostics ### Failure mode: login completed false Behavior: - keep explicit error message - invalidate login session - refresh snapshot once ### Failure mode: logout failure Behavior: - keep current snapshot - surface error ### Failure mode: app restart or shutdown during pending login Behavior: - next app session starts from `idle` - no persisted phantom pending state - fresh snapshot determines whether login actually completed elsewhere or must be retried ### Failure mode: rate-limits failure Behavior: - degrade only the rate-limit panel - do not mark account disconnected ## Diagnostics And Logging Policy We want enough observability to debug auth issues, but not enough to leak secrets or create false confidence from logs. ### Recommended structured events Add additive logs around: - account snapshot refresh started - account snapshot refresh settled - login started - login browser open attempted - login completed - login cancelled - logout started - logout settled - launch readiness resolved - execution auth mode resolved - degraded account read ### Safe fields to log - provider id - backend id - preferred auth mode - effective auth mode - snapshot state - readiness state - requiresOpenaiAuth - binary available - degraded reason category ### Fields to avoid logging verbatim - `authUrl` - API keys - refresh tokens - full account email if logging policy treats it as sensitive - raw `loginId` unless support/debug mode explicitly needs it Recommended compromise for email: - either do not log it - or log only a redacted form for support diagnostics Recommended compromise for `loginId`: - either do not log it - or log only a short fingerprint / suffix that cannot be used as a live control token Recommended compromise for `authUrl`: - log at most: - URL scheme - hostname - whether validation passed - never log query parameters or full path verbatim ### Important anti-lie rule Logs must distinguish: - `app-server degraded` - `managed account absent` - `execution failed` These are different operational truths and must not be collapsed into one generic auth error. ## Telemetry And Operational Metrics We do not need heavy telemetry to build the feature, but we should design enough observability to see whether rollout is healthy. ### Recommended counters - `codex_account_snapshot_refresh_total` - `codex_account_snapshot_refresh_degraded_total` - `codex_account_login_start_total` - `codex_account_login_success_total` - `codex_account_login_failure_total` - `codex_account_login_cancel_total` - `codex_account_logout_total` - `codex_account_launch_ready_chatgpt_total` - `codex_account_launch_ready_api_key_total` - `codex_account_launch_missing_auth_total` ### Recommended timers / histograms - snapshot refresh latency - app-server initialize latency - `account/read` latency - login time to completion - logout latency ### Recommended rollout health signals During rollout, we should watch for: - degraded refresh rate - login failure rate - mismatch rate between expected and effective auth mode - launch failures after a `ready_chatgpt` decision ### Anti-goal Do not let telemetry become a hidden source of truth for auth semantics. It is for diagnosis only, not for deciding whether the user is connected. ## Troubleshooting Playbook When this feature misbehaves, these are the most likely symptom clusters and where to look first. ### Symptom - UI says ChatGPT account connected, but launch behaves like API key Check: - ChatGPT launch env sanitization - `forced_login_method="chatgpt"` override actually being passed - whether ambient `OPENAI_API_KEY` or `CODEX_API_KEY` survived into exec env Likely owner: - `CodexConnectionCoordinator` - `CodexAccountEnvBuilder` ### Symptom - UI loses account state after a transient refresh failure Check: - degraded-path handling - freshness window logic - last-known-good snapshot clearing behavior Likely owner: - snapshot merge use case - cache / freshness policy ### Symptom - login opens browser twice or gets stuck pending Check: - login session exclusivity guard - duplicate-click handling - notification delivery and timeout cleanup Likely owner: - `CodexLoginSessionManager` ### Symptom - login never resumes after app restart Check: - whether pending login was incorrectly persisted - whether startup incorrectly restored stale renderer-local login state - whether the first fresh snapshot after restart was skipped Likely owner: - startup recovery policy - main-process feature initialization ### Symptom - cancel appears to work, but account still becomes connected Check: - whether cancel raced with a login that had already completed upstream - whether the final post-cancel snapshot was used as the source of truth - whether UI incorrectly treated cancel intent as stronger than fresh steady-state truth Likely owner: - operation serialization policy - login session manager - post-mutation snapshot settlement ### Symptom - renderer surfaces disagree about current state Check: - feature view-model adapters - whether one surface is reading raw provider status instead of the feature snapshot - whether stale event ordering is overriding newer state Likely owner: - renderer adapters / hooks - shell composition boundary ### Symptom - app-server sees account, but exec does not Check: - resolved `HOME` - resolved `USERPROFILE` - resolved `CODEX_HOME` - whether app-server and exec are built from the same auth-root normalization path Likely owner: - env builder - shell env source adapter ## Security And Privacy Rules These are non-negotiable. 1. Do not parse `auth.json`. 2. Do not copy Codex-managed tokens into app storage. 3. Do not log auth URLs verbatim if they may include sensitive values. 4. Do not persist login session ids beyond process lifetime. 5. Keep account metadata in memory by default. 6. Keep API keys in the app's existing secure storage only. 7. Do not use `account/login/start { type: "apiKey" }` in the first implementation. 8. Do not send raw `authUrl` over IPC to renderer. 9. Do not persist `loginId` or pending login state across restarts. 10. Validate login URLs with a feature-specific `https`-only policy before browser open. ### Why we should not use app-server API-key login mode Because it would create two overlapping secret owners: - app secure storage - Codex internal storage That violates: - DRY - single responsibility - clear ownership So the rule is: - managed ChatGPT account is owned by Codex - API key is owned by the app ## Security Review Checklist Before rollout, the implementation should be reviewed against this checklist. ### Secret handling - no API key written into feature logs - no auth URL logged verbatim - no ChatGPT managed token copied into app storage - no legacy auth files parsed directly ### Process/env handling - ChatGPT launches strip `OPENAI_API_KEY` - ChatGPT launches strip `CODEX_API_KEY` - API-key launches inject only the intended key - app-server and exec use the same auth-store env roots ### Persistence handling - only `preferredAuthMode` is persisted for Codex connection preference - login ids are not persisted across process lifetime - last-known-good managed account cache is memory-only ### UI honesty - degraded control-plane state is not shown as confirmed logout - API-key availability is not shown as managed-account connection - managed-account connection is not shown as API-key billing ### Rollout safety - no hidden automatic fallback from ChatGPT launch failure to API-key launch - no normal UI path silently invokes legacy Codex transport ## Testing Strategy ## Unit tests - feature core Add tests for: - managed account + API key merge rules - effective auth mode resolution - launch readiness resolution - degraded-state behavior - migration defaults ## Unit tests - main infrastructure Add tests for: - `CodexAccountEnvBuilder` - `CodexAccountAppServerClient` - `CodexLoginSessionManager` - cache TTL and single-flight behavior - app-server timeout handling - env sanitization rules Critical cases: 1. managed account exists and ambient API key is present 2. ChatGPT mode must strip API keys 3. API-key mode must inject API key 4. account read must not inherit ambient API keys 5. same HOME / USERPROFILE / CODEX_HOME resolution used for read and exec ## Unit tests - renderer Add tests for: - ChatGPT connected state - API-key only state - both-available state - degraded state - runtime missing state - login pending state - plan type display - rate-limit panel visibility ## Integration tests - shell Update existing shell tests: - `ProviderRuntimeSettingsDialog.test.ts` - `CliStatusVisibility.test.ts` - `providerAwareCliEnv.test.ts` - `ProviderConnectionService.test.ts` - `ClaudeMultimodelBridgeService.test.ts` ## Test Matrix - must-cover scenarios This matrix should be used to ensure we are not only testing the happy path. | Scenario | Snapshot expectation | Launch expectation | UI expectation | Test level | | --- | --- | --- | --- | --- | | Binary missing | `runtime_missing` | blocked | login disabled, missing runtime copy | unit + renderer | | Managed account only | `managed_account_connected` | ChatGPT launch | plan/email visible | unit + integration | | API key only | `api_key_available` | API-key launch | API key available copy | unit + integration | | Both available with `auto` | `both_available` | ChatGPT launch | Auto prefers ChatGPT | unit + integration | | Both available with `api_key` | `both_available` | API-key launch | API key preferred copy | unit + integration | | Managed account detected but app-server degraded | `degraded` | warning launchable | degraded banner, not false logout | unit + integration | | No auth and app-server degraded | `degraded` or `not_connected` depending freshness | blocked unless freshness rule applies | unable to verify copy | unit | | App-server stable handshake incompatible | feature locked or degraded with incompatibility verdict | no misleading login affordance | update-runtime / incompatible-runtime copy | unit + integration | | Login pending | `login_in_progress` or pending login state | no duplicate login start | cancel action visible | unit + renderer | | Two renderer subscribers during one login flow | shared pending state in both surfaces | one login only | consistent pending/cancel UI in both places | unit + renderer integration | | Pending login lost on app restart | fresh snapshot wins, no phantom pending state | login can be retried cleanly | informational recovery or silent idle reset | unit + integration | | Unsafe login URL returned | login fails before open | no browser open side effect | explicit safe error state | unit | | Cancel races with late login completion | freshest post-race snapshot wins | no forced false logout | pending clears into truthful connected or idle state | unit + integration | | Logout races with stale pre-logout read | post-logout truth wins | no resurrection of old account | disconnected state remains stable | unit + integration | | Corrupted legacy Codex config subtree | normalizes to safe default | no launch-preference crash | settings load with sane default copy | unit | | Browser open failure | failed login state | no launch behavior change | explicit error surfaced | unit + renderer | | Logout success | no managed account | auto may fall back to API key | UI clears managed account | integration | | Managed workspace restriction | no phantom managed account | launch blocked with policy truth | workspace-restricted copy, no fake workspace switcher | unit + renderer | | Stale slow read after fast successful read | latest successful snapshot preserved | none | no regression flicker | unit | | Ambient API key present during ChatGPT launch | managed account still primary | keys stripped | no API-key-primary wording | unit + integration | ## Test doubles and harness requirements To keep the implementation testable, we should plan the following fakes: - fake app-server client for deterministic `account/read`, `account/logout`, and rate-limit reads - fake app-server initialize handshake that can: - succeed with stable account support - succeed but reject required methods as incompatible - return diagnostic `codexHome` / platform metadata - fake login session transport that can emit: - success - failure - timeout - duplicate notification - late success after cancel intent - fake browser launcher that can: - succeed - fail - fake login URL validator that can: - accept valid `https` URLs - reject unsafe schemes or malformed values - fake clock for TTL and freshness testing - fake API key source adapter - fake shell env source for `HOME` / `USERPROFILE` / `CODEX_HOME` determinism - fake config input fixtures covering: - missing Codex subtree - stale legacy keys - malformed non-object Codex subtree Important rule: - do not make most tests spawn the real `codex app-server` - reserve real app-server checks for live signoff and a very small number of high-value integration tests ## Live signoff Required live or semi-live signoff: 1. already logged-in ChatGPT account autodetects without relogin 2. launch succeeds with ChatGPT auth and no API key 3. launch succeeds with API key mode 4. both-available state prefers ChatGPT in auto mode 5. chatgpt mode strips API keys from the exec env 6. login opens browser and completes 7. logout clears managed account state 8. initialize diagnostics capture the expected `codexHome` and compatibility verdict 9. if a managed-policy environment is available, workspace restriction surfaces policy-specific copy 10. restarting during a pending login does not leave the next app session stuck pending 8. app-server degradation does not falsely report logged out 9. app-server degradation does not falsely hard-block `codex exec` ## Manual QA And Failure Injection Checklist In addition to automated tests, the following manual checks are high value because they exercise real process boundaries and browser behavior. ### Happy-path manual checks - open the app with an already logged-in ChatGPT-backed Codex account - verify autodetect without relogin - verify `auto` chooses ChatGPT - verify explicit `api_key` preference still works when an API key exists ### Failure-injection manual checks - break or suspend app-server startup and verify the UI shows `degraded`, not false logout - remove the stored API key and verify `api_key` preference becomes `missing_auth` - keep an ambient API key in the shell env and verify explicit ChatGPT launch still strips it - trigger login and close/cancel before completion - trigger login and force browser-open failure if possible via a fake or test harness - logout and verify stale degraded reads do not resurrect the old account state ### Visual parity checks - compare dashboard banner, settings section, and manage dialog for the same state - verify wording consistency for: - connected ChatGPT account - API-key-only availability - both available - degraded - runtime missing ## Signoff Artifacts And Evidence Discipline To keep rollout transparent, each significant phase should leave behind explicit evidence. ### Recommended evidence file Create and maintain: - `docs/research/codex-app-server-account-signoff-evidence.md` ### What evidence should include For each completed phase: - date - branch / commit SHA - what scenarios were exercised - what test suites were run - what live manual checks were run - whether any known gaps remain ### Minimum evidence snippets Capture: - one managed-account autodetect result - one API-key-only result - one both-available result - one degraded-path result - one ChatGPT launch proof with API keys absent from effective exec env ### Anti-handwave rule Do not mark a phase "done" based only on: - unit tests - visual inspection - a single happy-path local login We need evidence that the failure and degraded paths behave truthfully too. ## Recommended Signoff Commands These commands should be treated as the default signoff baseline for this repo unless implementation details require a small adjustment. ### Targeted runtime and UI suite ```bash pnpm test -- \ test/main/services/runtime/providerAwareCliEnv.test.ts \ test/main/services/runtime/ProviderConnectionService.test.ts \ test/main/services/runtime/ClaudeMultimodelBridgeService.test.ts \ test/main/ipc/configValidation.test.ts \ test/renderer/components/runtime/ProviderRuntimeSettingsDialog.test.ts \ test/renderer/components/runtime/providerConnectionUi.test.ts \ test/renderer/components/cli/CliStatusVisibility.test.ts ``` ### Targeted `recent-projects` safety suite ```bash pnpm test -- \ test/features/recent-projects/main/infrastructure/CodexAppServerClient.test.ts \ test/features/recent-projects/main/adapters/output/CodexRecentProjectsSourceAdapter.test.ts \ test/features/recent-projects/core/application/ListDashboardRecentProjectsUseCase.test.ts \ test/features/recent-projects/contracts/normalizeDashboardRecentProjectsPayload.test.ts \ test/features/recent-projects/renderer/adapters/RecentProjectsSectionAdapter.test.ts ``` ### Typecheck baseline ```bash pnpm typecheck ``` ### Lint baseline ```bash pnpm lint ``` ### Full repo safety pass before wider rollout ```bash pnpm test pnpm typecheck pnpm lint ``` ### Live manual signoff There is no single universal script for this yet, so the plan should expect a small manual desktop signoff pass covering: - autodetect existing ChatGPT account - ChatGPT-backed launch without API key - API-key-backed launch - degraded-path truthfulness - logout and stale-state non-resurrection ## Release Readiness Checklist Before wider rollout or merge into the main delivery branch, the implementation should satisfy this checklist end-to-end. ### Contract readiness - feature contracts compile cleanly - preload bridge matches contracts - renderer hooks consume only the supported API surface ### Runtime readiness - ChatGPT and API-key launches both work - exec env sanitization is verified - app-server degradation is truthful and non-destructive ### UI readiness - dashboard, settings, and manage dialog agree on the same state - Codex wording no longer flattens everything to API key - terminal-modal Codex login path is removed or intentionally retained only per rollout policy ### Safety readiness - security checklist is green - troubleshooting playbook scenarios have at least been spot-checked - signoff evidence file has current results ### Regression readiness - targeted runtime/UI suites pass - targeted `recent-projects` safety suite passes - typecheck and lint pass ## Post-Release Triage Checklist If issues appear after internal rollout, use this triage order before making design changes. ### 1. Determine the symptom class - wrong UI state - wrong launch auth mode - login lifecycle bug - degraded-state regression - rate-limit-only issue ### 2. Confirm whether steady-state snapshot is correct Ask: - does a forced fresh snapshot show the right account truth? If yes: - the bug is more likely in event ordering, caching, or renderer composition If no: - the bug is more likely in control-plane reads, env routing, or migration/config ### 3. Confirm whether execution env matches launch decision Ask: - did the launch decision say `chatgpt` or `api_key`? - did the actual exec env honor that? ### 4. Confirm whether auth-store roots match Ask: - did app-server and exec use the same `HOME` / `USERPROFILE` / `CODEX_HOME`? ### 5. Only then consider rollback or feature hiding The first response should usually be: - diagnose - patch the narrow seam not: - broaden fallback - revive legacy transport ## Rollout Plan ## Recommended rollout shape 1. land feature behind internal enablement 2. wire read-only autodetect first 3. wire launch policy next 4. wire login/logout UI after launch policy is correct 5. remove Codex terminal modal login path only after feature login is green ## Why this order matters The most dangerous intermediate state is: - UI claims subscription support - runtime still hard-requires API key So launch policy must land before or together with user-facing subscription affordances. ## Safe Disable And Rollback Policy This feature should be additive enough that we can disable exposure without corrupting runtime truth. ### Safe disable rule If the feature must be temporarily hidden during rollout: - hide Codex managed-account UI affordances - keep persisted `preferredAuthMode` readable - do not auto-rewrite user preference - do not silently remap ChatGPT preference to API-key preference ### What rollback must not do Do not respond to rollout stress by: - reintroducing legacy Codex transport - auto-falling back from failed ChatGPT launch to API key without user intent - erasing Codex account metadata from config or UI state ### Acceptable temporary rollback shape If we need a temporary rollback during early rollout: - feature UI can be hidden or marked internal - launch policy can remain additive and guarded - the old terminal-modal Codex login path may remain only until the feature is fully proven This preserves correctness without lying about runtime semantics. ## Remaining Open Questions And Recommended Defaults The plan is intentionally decisive, but a few implementation choices can still remain configurable. These should not block the first pass because we already have recommended defaults. ### Open question 1 - exact freshness window duration Recommended default: - `60 seconds` Rationale: - long enough to survive brief app-server instability - short enough to avoid long-lived stale-account lies ### Open question 2 - whether rate limits should auto-load or stay lazy Recommended default: - lazy-load rate limits Rationale: - account summary and launch readiness are higher priority - rate limits should not slow the base snapshot path ### Open question 3 - whether device-code login should land in phase 1 Recommended default: - no, keep it deferred Rationale: - browser flow is more aligned with the intended UX - device code adds scope without unblocking the primary desktop path ### Open question 4 - whether to expose degraded state as a separate badge vs text-only warning Recommended default: - keep degraded as explicit text/state first, add badge only if it materially improves clarity Rationale: - textual honesty is more important than visual flourish ### Open question 5 - whether to hard-pin a minimum Codex binary version Recommended default: - no hard semver pin for the first wave beyond diagnostics and troubleshooting Rationale: - protocol/capability handshake is a safer unlock mechanism than guessing from version strings - hard pins are still possible later if rollout evidence shows a real compatibility floor ### Open question 6 - whether Codex account snapshot should be folded more deeply into generic runtime status Recommended default: - no, keep it as a separate feature snapshot and compose at the shell boundary Rationale: - preserves clean bounded contexts - avoids poisoning generic provider contracts with Codex-specific semantics ### Open question 7 - whether to hard-allowlist ChatGPT login hostnames Recommended default: - no hard hostname allowlist in the first wave, only strict `https` validation plus redacted diagnostics Rationale: - current docs and schema establish the existence of `authUrl`, but not a future-proof hostname contract - strict scheme validation gives strong safety without coupling the feature to undocumented host choices ### Open question 8 - whether explicit user refresh should immediately escalate to `refreshToken = true` Recommended default: - no, start with a normal read and escalate only on concrete auth-staleness evidence Rationale: - keeps manual refresh predictable without overusing token refresh as a blunt instrument - preserves a clear distinction between ordinary state refresh and auth recovery ## Risk Register This section lists the most important remaining implementation risks and how the plan contains them. ### Risk 1 - false subscription billing semantics Failure shape: - UI says ChatGPT account is active - launch silently uses API key Mitigation: - ChatGPT execution must strip `OPENAI_API_KEY` and `CODEX_API_KEY` - force `forced_login_method="chatgpt"` - add dedicated tests for this exact case ### Risk 2 - split auth store between control plane and execution Failure shape: - app-server sees logged-in account - `codex exec` uses different `HOME` / `CODEX_HOME` Mitigation: - centralize env resolution - test exact parity of resolved auth store env for read/login/logout/exec ### Risk 3 - false logout on transient app-server failure Failure shape: - timeout or initialize failure - UI collapses to `not connected` Mitigation: - degraded state is first-class - freshness window can preserve last-known-good account truth temporarily ### Risk 4 - shell regains ownership and recreates coupling Failure shape: - banner/dialog/section each re-implement Codex logic differently Mitigation: - feature owns Codex semantics - shell only hosts composition and generic layout ### Risk 5 - config drift creates silent preference flips Failure shape: - presence of API key or account implicitly changes user preference Mitigation: - explicit separation of persisted preference and observed availability - one-way normalization with no inferred preference writes ### Risk 6 - rollout exposes login before launch policy is correct Failure shape: - user can log into ChatGPT in UI - runtime still blocks without API key Mitigation: - rollout gate order enforces launch policy before or alongside subscription UX ### Risk 7 - over-expansion of scope into browser mode or apiKey app-server login Failure shape: - feature accumulates multiple control planes in first wave Mitigation: - browser mode explicitly deferred - app-server `apiKey` login explicitly out of first implementation ## Follow-On Plan For `agent_teams_orchestrator` This feature is intentionally scoped to `claude_team` first, but we should document the expected later parity path now so the first implementation does not paint us into a corner. ### What should carry over later - normalized Codex auth vocabulary - `preferredAuthMode` - `effectiveAuthMode` - `degraded` - managed-account vs API-key truth - same launch-readiness semantics - same exec env sanitization rules - same no-silent-fallback rule ### What should not be copied blindly - Electron-specific browser launch adapters - preload contracts - renderer hook design ### Suggested parity order 1. share only pure policy and data-shape concepts first 2. extract any provider-agnostic launch-readiness helpers only if duplication is real 3. keep transport/UI/process integration repo-specific ### Important guardrail Do not prematurely contort the `claude_team` implementation around orchestrator parity if it makes the desktop feature worse or harder to reason about. ## Phase Gates And Go / No-Go Rules This section is the release-quality version of the rollout. ### Gate 1 - read-only truth is trustworthy Must be true before exposing any new login affordance: - autodetect works for already logged-in ChatGPT users - API key availability still shows correctly - degraded account reads do not erase last-known-good truth immediately - no current non-Codex provider behavior regresses ### Gate 2 - launch policy is trustworthy Must be true before defaulting UI toward subscription messaging: - ChatGPT-backed launch works with no API key present - ChatGPT-backed launch strips API keys from env - API-key launch still works - auto mode resolves deterministically and observably ### Gate 3 - interactive login is trustworthy Must be true before removing terminal-modal login for Codex: - browser login starts reliably - duplicate clicks do not create duplicate sessions - cancel works - logout works - stale cached snapshots do not reappear after login/logout transitions ### Gate 4 - UI parity is trustworthy Must be true before calling the feature complete: - no normal Codex UI path still says API key is the only connection mechanism - dashboard and settings surfaces agree on account truth - the rendered Codex panel uses the same account truth as launch policy ### Hard no-go conditions Do not ship the feature if any of these remain true: - ChatGPT mode can still execute with inherited API keys - app-server and exec use different auth storage roots - degraded app-server reads collapse to false logout - `ProviderConnectionService` remains the final readiness authority for Codex - Codex login in normal UI still routes through terminal modal commands ## Implementation Phases And Commit Boundaries ### Phase A - shared transport extraction Goal: - extract reusable app-server transport primitives from `recent-projects` Estimated size: - `250-450` lines Suggested commit: - `refactor(codex-account): extract shared app-server transport` Primary deliverables: - extracted generic JSON-RPC stdio primitives - no deep import from `recent-projects` into the new feature - `recent-projects` still green on the extracted transport Acceptance criteria: - no behavior change for `recent-projects` - extracted transport owns initialize/initialized handshake - transport defaults are centralized in one place ### Phase B - feature skeleton and read-only snapshot Goal: - create the feature slice - implement `account/read` - implement snapshot DTO and IPC bridge Estimated size: - `450-750` lines Suggested commit: - `feat(codex-account): add managed account snapshot feature` Primary deliverables: - feature slice exists with contracts, main, preload, renderer entrypoints - `account/read` wired through feature IPC - API key availability merged into snapshot - cached snapshot and degraded state logic working Acceptance criteria: - already logged-in ChatGPT user is detected - API-key-only user is detected - degraded app-server read does not present false logout ### Phase C - config, migration, and renderer composition Goal: - add `preferredAuthMode` - integrate feature panel into runtime settings and provider cards Estimated size: - `300-550` lines Suggested commit: - `feat(codex-account): add codex auth preference and shell composition` Primary deliverables: - persisted `preferredAuthMode` - config migration on load and save - shell surfaces render the feature-owned Codex panel Acceptance criteria: - stale legacy Codex config normalizes forward - renderer can display auto/chatgpt/api_key preference correctly - generic shell layout remains unchanged for non-Codex providers ### Phase D - runtime launch policy integration Goal: - remove API-key-only hard gate - add env sanitization and `forced_login_method` policy Estimated size: - `300-600` lines Suggested commit: - `feat(codex-account): wire codex launch readiness policy` Primary deliverables: - Codex launch readiness no longer owned by API-key-only logic - ChatGPT env sanitization wired - `forced_login_method` wired per effective auth mode Acceptance criteria: - ChatGPT launch works without API key - API-key launch still works - ambient API keys cannot hijack ChatGPT launch mode ### Phase E - login, cancel, logout Goal: - full managed login lifecycle via app-server Estimated size: - `350-650` lines Suggested commit: - `feat(codex-account): add codex app-server login lifecycle` Primary deliverables: - browser login flow - cancel flow - logout flow - live login session manager with timeout and duplicate-click safety Acceptance criteria: - one login session at a time - login success refreshes snapshot - logout clears managed account state in UI ### Phase F - rate limits and final shell cleanup Goal: - add rate limits - remove Codex terminal modal auth path Estimated size: - `150-300` lines Suggested commit: - `refactor(codex-account): finalize native codex account ui` Primary deliverables: - rate-limit panel - final Codex copy cleanup - terminal-modal Codex login/logout removed from normal UI paths Acceptance criteria: - dashboard and settings show consistent Codex account story - plan type and rate limits are visible when available - no normal Codex UI path still presents API key as the only connection method ## Phase-By-Phase Task Checklist This section gives the recommended execution order inside each phase so implementation can proceed with fewer ambiguous jumps. ### Phase A checklist 1. extract generic JSON-RPC stdio transport primitives out of `recent-projects` 2. move initialize/initialized handshake helpers into shared Codex app-server infrastructure 3. repoint `recent-projects` to the extracted transport 4. verify no behavior change in `recent-projects` ### Phase B checklist 1. scaffold `src/features/codex-account` public entrypoints 2. define DTOs, channels, and event contracts 3. implement `account/read` client 4. implement managed-account plus API-key merge logic 5. add cache and single-flight snapshot reads 6. register IPC and preload bridge 7. verify read-only snapshot in renderer/devtools path ### Phase C checklist 1. add `providerConnections.codex.preferredAuthMode` to config types 2. implement read-time and write-time normalization 3. update validation to the new shape 4. create feature-owned Codex panel and adapters 5. integrate panel into runtime settings/manage surfaces 6. remove API-key-only wording from Codex-specific renderer paths ### Phase D checklist 1. implement launch-readiness use case 2. implement auth-specific exec env policy 3. introduce runtime coordinator for Codex launch decisions 4. delegate Codex env assembly away from API-key-only logic 5. verify ChatGPT launch, API-key launch, and degraded-path behavior ### Phase E checklist 1. implement login session manager 2. implement browser login start flow 3. implement cancel flow 4. implement logout flow 5. wire login events into snapshot refresh and renderer subscriptions 6. verify duplicate-click safety and timeout behavior ### Phase F checklist 1. implement lazy rate-limit reads and cache 2. surface plan/rate-limit info in feature UI 3. remove normal Codex terminal-modal login/logout path 4. harmonize dashboard/settings/manage wording 5. capture final signoff evidence and residual known gaps ## PR Slicing And Review Discipline Even with a strong plan, this feature can create bugs if we ship overly broad mixed PRs. ### Recommended slicing rule Prefer one phase per PR when possible. If a phase becomes too wide, split it by seam, not by random files. Good split examples: - transport extraction - read-only snapshot and IPC - config migration and shell composition - runtime launch policy - login lifecycle - rate limits and cleanup Bad split examples: - "main changes" vs "renderer changes" when both are needed to make one behavior coherent - mixing migration, login, and runtime policy into one giant PR ### Review checklist for each PR Every PR should answer: 1. what is the new source of truth introduced or changed 2. what existing source of truth stops owning that behavior 3. what failure mode is newly covered 4. what migration risk exists 5. what tests prove the behavior ### Green-state rule Each PR should leave the app in a coherent state: - no UI promise without runtime support - no runtime support hidden behind stale UI wording - no half-migrated config shape exposed to renderer code ## Implementation Anti-Patterns To Avoid These are the failure modes most likely to reintroduce the problems this plan is trying to solve. ### Anti-pattern 1 - generic shell service quietly regains Codex policy Bad shape: - `ProviderConnectionService` or `ClaudeMultimodelBridgeService` starts accumulating special-case Codex account logic again Why it is bad: - recreates the coupling we are explicitly trying to remove ### Anti-pattern 2 - renderer computes business truth from badges and booleans Bad shape: - UI derives Codex auth semantics from generic `authenticated`, `authMethod`, or `statusMessage` flags alone Why it is bad: - produces inconsistent copy and launch assumptions across surfaces ### Anti-pattern 3 - migration infers preference from availability Bad shape: - existing API key or managed account silently rewrites `preferredAuthMode` Why it is bad: - mutates user intent based on incidental environment state ### Anti-pattern 4 - degraded becomes a synonym for connected Bad shape: - stale evidence is treated as permanently sufficient proof of current auth state Why it is bad: - creates false readiness and false billing semantics ### Anti-pattern 5 - launch fallback becomes silent Bad shape: - failed ChatGPT launch silently retries with API key Why it is bad: - destroys trust in the runtime story and billing expectations ## Definition Of Done This feature is done only when all of the following are true: 1. A previously logged-in ChatGPT Codex account autodetects automatically. 2. The UI clearly distinguishes managed account and API key availability. 3. `auto` mode works and prefers ChatGPT when available. 4. Launch policy no longer falsely requires API key when managed account exists. 5. ChatGPT-mode execution sanitizes API-key env vars. 6. API-key mode still works. 7. Login, cancel, and logout work from the real UI. 8. Codex terminal-login path is no longer used in normal UI flows. 9. `recent-projects` remains green. 10. Existing non-Codex provider UX remains unchanged. ## Explicit Rejections The following approaches are intentionally rejected. ### Rejected - read `~/.codex/auth.json` Reason: - brittle - storage backend may vary - security-sensitive ### Rejected - revive legacy Codex OAuth transport Reason: - wrong architecture - incompatible with native-only cutover intent ### Rejected - put all Codex account logic into `ProviderConnectionService` Reason: - violates SRP - creates provider-specific policy blob - fights the feature architecture standard ### Rejected - hard-block launch whenever app-server is degraded Reason: - false negative risk - execution seam and account control plane are different ### Rejected - use app-server API-key login mode in the first wave Reason: - creates dual key stores - unclear ownership ## Final Recommendation Implement this as a full feature slice with: - extracted shared app-server transport primitives - app-server-managed account truth - app-owned API key truth - feature-owned launch-readiness policy - shell integration through composition, not more Codex branches in shell components The critical correctness points are: - unify auth storage context across app-server and exec - sanitize API-key env vars in ChatGPT mode - do not let app-server degradation become a false execution blocker If those three rules are followed, this plan should fit the orchestrator/UI architecture cleanly and restore the right Codex UX without reintroducing the old legacy path.