From efabcaa362216ef52a8bb11560155112fd1effd0 Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Sat, 1 Aug 2026 18:05:52 +0900 Subject: [PATCH 01/63] docs: design native main profile switching --- .../260801_native_main_profiles/000_design.md | 466 ++++++++++++++++++ 1 file changed, 466 insertions(+) create mode 100644 devlog/_plan/260801_native_main_profiles/000_design.md diff --git a/devlog/_plan/260801_native_main_profiles/000_design.md b/devlog/_plan/260801_native_main_profiles/000_design.md new file mode 100644 index 000000000..b149c230e --- /dev/null +++ b/devlog/_plan/260801_native_main_profiles/000_design.md @@ -0,0 +1,466 @@ +# Native main profile switching design spike (#656) + +Status: fork-only design spike, not implementation-ready + +Issue: https://github.com/lidge-jun/opencodex/issues/656 + +Official Codex reference: `openai/codex@ee0247f95a6fe2b094ba2253d82cae2a2b4c2dff` + +## Decision summary + +The first implementation should be CLI/backend-only and should use these safety boundaries: + +1. Support native ChatGPT credentials only when the effective Codex credential store is `file`. +2. Treat the complete native auth envelope as opaque bytes. Parse it for validation, but never rebuild it from Pool fields. +3. Keep exactly one refresh owner. Official Codex owns the active profile; the encrypted OpenCodex vault owns inactive profiles. +4. Move a profile between those owners transactionally. Do not create a second independently refreshable Pool record. +5. Require encrypted storage for inactive profiles, backed by an OS-protected key. There is no plaintext fallback. +6. Bind every vault and switch transaction to one canonical effective `CODEX_HOME`. +7. Refuse a live switch while a native Codex process can still refresh the old credential. Drain OpenCodex `__main__` traffic and require a native Codex restart. +8. Preserve task and history data. Only the active credential and `__main__` runtime-derived state may change. +9. Prove rollback for every failure after the first credential write before opening a behavior-changing PR. +10. Keep GUI work out of the first PR. + +This design deliberately rejects `keyring`, `auto`, and `ephemeral` Codex credential modes in v1. Supporting those modes requires an official Codex integration point; directly reproducing Codex keyring internals in TypeScript would be brittle and unsafe. + +## Why the Pool credential model cannot be reused + +The current Pool record contains the fields OpenCodex needs to route and refresh Pool traffic. It is not the complete native Codex credential record. + +The current official Codex `AuthDotJson` includes: + +- `auth_mode` +- `OPENAI_API_KEY` +- `tokens` +- `last_refresh` +- `agent_identity` +- `personal_access_token` +- `bedrock_api_key` + +The ChatGPT token payload includes the raw ID token, access token, refresh token, and optional account ID. The format can gain fields independently of OpenCodex. + +Relevant official definitions: + +- [AuthDotJson](https://github.com/openai/codex/blob/ee0247f95a6fe2b094ba2253d82cae2a2b4c2dff/codex-rs/login/src/auth/storage.rs#L38) +- [TokenData](https://github.com/openai/codex/blob/ee0247f95a6fe2b094ba2253d82cae2a2b4c2dff/codex-rs/login/src/token_data.rs#L10) + +Converting a Pool record into `auth.json` would lose native fields and assign refresh ownership to two independent implementations. That is not a supported conversion path. + +## Official Codex storage constraints + +Official Codex currently has four credential-store modes: + +| Mode | Official behavior | v1 behavior | +| --- | --- | --- | +| `file` | Stores credentials in `$CODEX_HOME/auth.json` | Supported | +| `keyring` | Stores credentials in the keyring and fails if unavailable | Refuse | +| `auto` | Prefers keyring and falls back to `auth.json` | Refuse because the active backend can change | +| `ephemeral` | Keeps credentials in process memory only | Refuse | + +The official default is currently `file`. On Windows, the default keyring backend stores encrypted secrets in a local file whose key is in the OS keyring. A successful keyring save can remove `auth.json`. + +Relevant official definitions: + +- [AuthCredentialsStoreMode](https://github.com/openai/codex/blob/ee0247f95a6fe2b094ba2253d82cae2a2b4c2dff/codex-rs/config/src/types.rs#L104) +- [AuthKeyringBackendKind](https://github.com/openai/codex/blob/ee0247f95a6fe2b094ba2253d82cae2a2b4c2dff/codex-rs/config/src/types.rs#L136) +- [Auth storage backends](https://github.com/openai/codex/blob/ee0247f95a6fe2b094ba2253d82cae2a2b4c2dff/codex-rs/login/src/auth/storage.rs#L163) + +Therefore, the presence or absence of `auth.json` alone does not identify the official active credential source. The capability probe must resolve the credential mode before any write. + +## Threat model + +The feature must protect against: + +- Token disclosure through logs, CLI output, management responses, crash journals, or diagnostic bundles. +- Writing to a different Codex home than the one shown to the user. +- Losing the original login after malformed input, failed validation, failed rename, failed ACL application, or failed read-back. +- Two components using the same rotating refresh token independently. +- A process crash between the auth-file replacement and the vault metadata update. +- A native Codex process refreshing the source profile while OpenCodex is switching it out. +- Stale in-flight `__main__` responses publishing usage or plan state for the old identity. +- Clearing tasks or history as a side effect of an account switch. +- Treating a missing, malformed, or temporarily unreadable credential as a confirmed logout or identity change. + +It does not claim to protect an unlocked account from malware running as the same OS user. The OS credential store and file ACLs are still required to reduce accidental disclosure and offline credential exposure. + +## Invariants + +### I1. Full-fidelity envelope + +OpenCodex stores the exact source bytes and their SHA-256 digest. It may parse a copy for validation, but it must write the original decrypted bytes without serializing a reduced TypeScript shape. + +This preserves fields that the installed Codex understands but the installed OpenCodex does not. + +### I2. One refresh owner + +Credential ownership is stateful: + +| Profile state | Credential owner | Refresh allowed by OpenCodex | +| --- | --- | --- | +| Active | Official Codex active store | No | +| Inactive | Encrypted OpenCodex vault | No | +| Staged login | Official Codex in a restricted temporary home | No | +| Switching | Transaction journal under an exclusive switch lock | No | + +OpenCodex never calls the OAuth refresh grant for native profiles. The Pool refresh implementation remains Pool-only. + +On a successful switch, the target encrypted payload is consumed from the inactive vault record and becomes the official active credential. The source active envelope becomes the encrypted inactive record. The target record retains metadata and an active marker, not a second refreshable payload. + +### I3. Exact home binding + +The operation resolves `CODEX_HOME` once, canonicalizes it, and computes: + +```text +home_id = SHA-256("opencodex-native-profile-home-v1\0" || canonical_home_bytes) +``` + +The vault, encrypted payload AAD, lock, and journal all carry this `home_id`. A mismatch is a hard error. An invalid explicit `CODEX_HOME` is never replaced with a fallback path. + +The CLI displays the canonical path used for the operation. The vault may store only `home_id`; it does not need to persist the raw path. + +### I4. No plaintext inactive profile + +After a successful command returns, an inactive native envelope exists only as authenticated ciphertext. Temporary plaintext created for staged official login is ACL-restricted and removed before success is reported. + +There is no `--allow-plaintext`, environment-variable bypass, or automatic fallback when key storage is unavailable. + +### I5. Unknown reads are not transitions + +Missing, malformed, and unreadable active credentials are `unknown` states. They stop the switch without changing the vault, runtime state, active profile marker, or task affinity. + +### I6. Commit before runtime reset + +`__main__` quota, cooldown, reauth, plan, cache, thread affinity, and WebSockets are cleared only after both the credential replacement and vault ownership transfer have committed. + +### I7. Task and history preservation + +No transaction step may write, move, truncate, or delete Codex task, history, rollout, session, or project files. A different account may be unable to continue a server-side task, but OpenCodex does not rewrite that task to hide the incompatibility. + +## Storage model + +The vault contains public metadata and authenticated ciphertext. Token-bearing fields are always inside `payload`. + +```ts +type NativeMainProfileVaultV1 = { + version: 1; + revision: number; + homeId: string; + activeProfileId: string | null; + profiles: NativeMainProfileRecordV1[]; +}; + +type NativeMainProfileRecordV1 = { + id: string; + label: string; + identityHash: string; + identityHint: string; + state: "active" | "inactive"; + payload: EncryptedEnvelopeV1 | null; + createdAt: string; + updatedAt: string; +}; + +type EncryptedEnvelopeV1 = { + cipher: "aes-256-gcm"; + keyRef: string; + nonce: string; + ciphertext: string; + tag: string; + envelopeSha256: string; +}; +``` + +`identityHint` is a user-safe, masked value. Email and raw account IDs are not required for listing. The authenticated additional data binds the ciphertext to: + +```text +format_version || home_id || profile_id || identity_hash || envelope_sha256 +``` + +The vault and journal use `atomicWriteFileAsync()` so Windows ACL application remains asynchronous and happens before publication by rename. + +## Key custody + +The implementation needs a narrow key-provider boundary: + +```ts +interface NativeProfileKeyProvider { + getOrCreate(homeId: string): Promise<{ + keyRef: string; + key: Uint8Array; + }>; +} +``` + +Requirements: + +- The production provider stores the random 256-bit master key in an OS-protected credential store. +- The vault file never contains the master key. +- A key-store failure is terminal and leaves native auth unchanged. +- Tests inject an in-memory deterministic provider. +- Decrypted buffers are short-lived and cleared on a best-effort basis. +- Crypto code uses a Bun-compatible `node:crypto` surface and is tested with both bundled Bun and Bun 1.4 canary. + +The current OpenCodex tree has no suitable cross-platform OS-keyring primitive. Selecting that dependency or platform adapter is a maintainer decision and is a blocking design gate for production code. A random key in a neighboring ACL-only file is not an acceptable substitute. + +## Credential capability probe + +Every mutating command runs the following read-only probe first: + +1. Resolve and canonicalize the effective `CODEX_HOME`. +2. Resolve the installed Codex credential-store mode. +3. Accept only an explicit or default `file` result. +4. Reject `keyring`, `auto`, and `ephemeral` with a specific error and no writes. +5. Read `auth.json` once and classify it as `ok`, `missing`, `invalid`, or `unreadable`. +6. Parse the complete JSON only to validate the ChatGPT envelope and derive a stable identity. +7. Report the canonical home, store mode, active identity hint, and feature availability without returning credentials. + +`auto` is rejected even when `auth.json` currently exists. Official Codex may later save to keyring and remove the file, so treating that file as authoritative would create split ownership. + +## Enrollment + +Two enrollment paths are needed. + +### Register the current active profile + +`ocx account main register