# OpenCodex authentication comparison This note records a read-only design comparison made on 2026-08-14 against the local OpenCodex source export at `/Users/chenyunzhe/Documents/Codex/2026-08-12/https-github-com-lidge-jun-opencodex/work/lidge-jun-opencodex-6d881db`. The export identifies itself as `@bitkyc08/opencodex` 2.12.0 under MIT. No OpenCodex state, Codex auth file, environment file, keychain, browser storage, database, or live login process was read or invoked. ## Read surface The comparison covered the root and `src` agent instructions; structure documents 00, 01, 02, 05, and 08; the ChatGPT OAuth, PKCE, loopback callback, OAuth registry, account store, token guardian, Codex auth API, state-store reconciliation, and redaction modules; the Codex and Provider OAuth GUI controllers; and focused callback, account-store, refresh, cancellation, status, and privacy tests. File SHA-256 values were captured locally so the reviewed export can be identified without importing its source. ## Adopted design ideas 1. **One Host-owned login transaction.** OpenCodex separates the protocol flow from its management projection. Its callback flow owns the random state, its provider flow owns the PKCE verifier, the loopback server receives the authorization code, and the token store owns access and refresh tokens. The UI receives only an authorization instruction and a token-free status. AuthInOne expresses the same responsibility split through its independently written `AuthProviderAdapter`, shared coordinator, and DSH credential store. Browser and device-flow adapters use one public start/status/cancel/submit/logout projection. 2. **Explicit terminal states.** OpenCodex combines a protocol-level pending/done/error record with Codex API states `starting`, `pending`, `done`, `error`, and `expired`; cancellation is terminal. AuthInOne exposes `signed-out`, `starting`, `pending`, `connected`, `cancelled`, `expired`, `needs-reauth`, and categorized `error` states. State, verifier, authorization code, device code, access token, and refresh token remain Host-only. 3. **Bounded browser and device flows.** The loopback pattern binds numeric loopback, handles IPv4/IPv6 ambiguity, validates path and state before consuming the flow, closes listeners in `finally`, and combines user cancellation with a fixed timeout. A matching Provider rejection consumes the transaction; a state mismatch does not. Device adapters follow the server polling interval, RFC 8628 `slow_down`, expiry, denial, cancellation, and Host-only device-code rules. 4. **Owner-only durable credential storage.** OpenCodex's generic OAuth and Codex account JSON stores are not encrypted vaults; they rely on owner-only directories/files, atomic replacement, validation, and hardened permissions. Its native-profile vault is a separate encrypted subsystem. AuthInOne does not reproduce either file schema. OAuth credentials live only in the DSH credentials provider, and the plugin never writes a token file itself. 5. **Refresh is a compare-and-swap operation.** OpenCodex deduplicates refresh per provider/account or refresh grant, re-reads durable state after acquiring a lock, persists rotated refresh tokens, and refuses a late result when the credential generation changed. AuthInOne uses one single-flight per Provider plus a credential-generation digest checked before storing a refresh result. A terminal grant error marks `needs-reauth`; transient failures remain retryable. 6. **Backoff is policy, not token logic.** OpenCodex's guardian calls the normal resolver, limits concurrency, uses capped exponential backoff, and keeps proactive refresh opt-in. AuthInOne needs no multi-account guardian. It refreshes lazily through the route's normal token resolver, bounds network attempts, and never lets a background retry keep the process alive. 7. **Remote DTOs are allowlists.** OpenCodex masks email/account identity, categorizes errors, and tests that access tokens, refresh tokens, codes, authorization fields, and upstream descriptions do not appear in status or logs. AuthInOne's Typert Remote carries only Provider id/name, method, risk label, redacted identity, authorization URL, user code when required, opaque attempt id, expiry deadline, status, and an allowlisted error category. ## Not copied or adopted - OpenCodex's local proxy, Codex-home injection, provider catalog routing, multi-account pool, account affinity, quota/cooldown machinery, native-main account import, local CLI/keychain discovery, warmup calls, request fingerprints, Claude compatibility, sidecars, and catalog convergence are product-specific and stay out of the DSH plugin. - Its source layout, class names, route names, DTOs, UI components, CSS, persistence schemas, constants table, error messages, and tests are not templates for AuthInOne. - OpenCodex documents that its callback helper and PKCE helper were ported from another project. AuthInOne does not reuse either implementation. - OAuth endpoint, client identity, redirect, scope, and wire parameter values come from reviewed public Provider implementations or formal dependencies. OpenCodex-specific values such as its originator label, chosen port, internal flow ids, manual-import route, or polling copy are not protocol authority. ## DSH placement | Concern | DSH owner | | --- | --- | | OAuth credential payload and refresh-token rotation | Host-only `ctx.credentials` reference | | Protocol adapter, loopback listener, state/PKCE, refresh single-flight, timeout, cancellation, disposal | AuthInOne Host plugin lifecycle | | Public login state and user actions | AuthInOne Typert Remote with an allowlisted DTO | | Entry and interaction | `settings.models.actions` contribution using DSH Modal/Button primitives | | Provider transport and model catalog | AuthInOne-owned adapter registration using the public DSH LLM API and reviewed formal Provider dependencies | | Refresh before model dispatch | AuthInOne transport resolves a fresh DSH-held access credential immediately before each request | | Usage and provider/model projections | Existing DSH session, settings, LLM, and credentials services | The generic `settings.models.actions`, `settings.models.insights`, and optional `settings.section.icon` contributions remain the only DSH core seams. No AuthInOne-specific OAuth or Provider business type belongs in DSH core. ## Dependency decision The first Auth draft imported `@earendil-works/pi-ai` directly for both OAuth and Codex transport. That monolithic dependency pulls unrelated provider SDKs, including `@google/genai` and `protobufjs`; their lifecycle-script declarations triggered the local pnpm build-policy gate. Marking those scripts as reviewed no-ops explains the failure but does not make the dependency surface minimal. AuthInOne implements its small OAuth and device adapters with Node/Web platform APIs where practical. It uses `@earendil-works/pi-ai` 0.82.1 under MIT for reviewed Provider transports and selected public-client protocol helpers, and `@cursor/sdk` 1.0.24 under the Cursor SDK License and Terms of Service for the Experimental Cursor transport. Both are bundled into the committed Host artifact at build time. Reusing the Web profile's existing `llm-pi-ai` instance was rejected because mutating the native instance would leave routing and uninstall ownership ambiguous. Every plugin-owned transport resolves the DSH-held access credential at request time and unregisters with the plugin lifecycle. Neither formal dependency becomes a separate DSH profile package. ## Provenance boundary OpenCodex was used only to compare responsibility boundaries and failure handling. AuthInOne code, tests, CSS, README prose, Typert DTOs, and credential representation remain independently designed. The OpenCodex repository is not a runtime dependency, and no source fragment is vendored.