diff --git a/README.md b/README.md index d434611..e5ccc6e 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,13 @@ The repository includes a Tauri shell, React frontend, isolated Rust core, the credential-broker CLI, cross-platform icons, CI, Dependabot, security policy, and contribution guide. +Proposed profile-management designs: + +- [Codex identity and history profile architecture](docs/codex-history-profile-architecture.md) +- [Codex credential rotation and synchronization architecture](docs/codex-token-management-architecture.md) +- [Cloud credential vault and broker feature](docs/cloud-credential-vault-feature.md) +- [Claude OAuth enrollment and automated reauthentication](docs/claude-oauth-enrollment-automation.md) + ## Windows installer Version tags publish Windows installers through the diff --git a/docs/claude-oauth-enrollment-automation.md b/docs/claude-oauth-enrollment-automation.md new file mode 100644 index 0000000..5d8bbe2 --- /dev/null +++ b/docs/claude-oauth-enrollment-automation.md @@ -0,0 +1,644 @@ +# Claude OAuth Enrollment and Automated Reauthentication + +Status: proposed +Target: openProfiler after `0.1.4` +Scope: authorized Claude Code profile enrollment, reauthentication, and +automation-token issuance through the official Claude CLI + +Related feature: [Cloud Credential Vault and Broker](cloud-credential-vault-feature.md) + +## Summary + +openProfiler should automate authorized Claude credential enrollment by +orchestrating Anthropic's supported Claude CLI, a restricted Microsoft 365 +mailbox reader, and an isolated Playwright browser. openProfiler must not +implement Anthropic's OAuth client, invent OAuth endpoints, or copy login state +from a general-purpose browser profile. + +The initial browser and email authorization produces either: + +1. a complete native Claude profile credential written by `claude auth login`; + or +2. a one-year, inference-only automation token printed by + `claude setup-token`. + +After a native profile has been enrolled, openProfiler can use the provider's +supported refresh-token provisioning variables to create a credential in an +isolated runtime without repeating email authorization. Every operation remains +subject to account authorization, exact identity binding, refresh-owner +leasing, and accepted-generation publication. + +## Authorization Gate + +This feature must not ship until the owning organization has confirmed that its +Anthropic agreement permits the proposed internal automation. + +Anthropic documents subscription OAuth as an authentication method for ordinary +use of native Anthropic applications, including Claude Code. Anthropic also +states that third-party developers must not offer Claude.ai login or route Free, +Pro, or Max credentials on behalf of users. openProfiler must obtain written +confirmation for its specific Team or Enterprise deployment, or use Claude +Console API keys, Microsoft Foundry, or another supported enterprise provider +instead. + +The approval applies only to authorized accounts and does not permit: + +- creating or accessing accounts without the account owner's authorization; +- bypassing CAPTCHA, MFA, SSO, phone verification, consent, or organization + policy; +- sharing one subscriber's credential with unrelated users; +- presenting openProfiler as Anthropic's OAuth client; +- routing non-Claude-Code traffic against subscription credentials; or +- extracting tokens from a browser session or undocumented provider endpoint. + +## Goals + +- Enroll an authorized Claude profile without exposing credentials to the React + webview, logs, telemetry, or operators. +- Reauthenticate a profile without risking its last accepted credential. +- Support native Claude profile credentials and supported automation tokens as + distinct credential formats. +- Read a single-use Claude login message from an explicitly scoped Exchange + Online mailbox. +- Complete browser authorization in an isolated, disposable Playwright context. +- Bind every accepted credential to the expected profile, account, and Claude + organization. +- Store accepted credentials as exact, versioned Azure Key Vault secrets. +- Coordinate exactly one refresh owner per provider account. +- Fail closed on ambiguous identity, unexpected organization selection, + provider-policy blocks, or browser challenges. + +## Non-Goals + +- openProfiler does not become an OAuth authorization server or OAuth client for + Anthropic. +- Mailbox possession alone does not prove the resulting Claude account or + organization identity. +- Exchange mailbox provisioning does not create a Claude account, assign a + subscription, or grant a Claude Code seat. +- The login worker does not receive Exchange Administrator, Global + Administrator, or direct Key Vault data-plane permissions. +- The system does not retain magic links, verification codes, browser cookies, + authorization URLs, or email bodies after a transaction. +- Automation does not retry CAPTCHA, MFA, SSO, phone, consent, or organization + policy failures. +- Key Vault presence does not prove that a credential remains valid. + +## Supported Credential Formats + +| Format | Provider operation | Storage and runtime use | Limitations | +| ---------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| `claude_native_oauth_bundle` | `claude auth login --claudeai --email
` | Store the complete `.credentials.json`; atomically materialize it into the selected profile | The owning Claude runtime may rotate the refresh token, so it requires a refresh-owner lease and publication watcher | +| `claude_code_oauth_token` | `claude setup-token` | Store the single token as a versioned secret; inject it only as `CLAUDE_CODE_OAUTH_TOKEN` into the launched process | One-year, inference-only credential; no Remote Control or claude.ai connector access; `--bare` does not use it | + +The two formats must not be merged. A native credential bundle is not created by +combining an automation token with fields from another generation. + +## Supported Provider Interface + +The Claude provider adapter uses only documented Claude CLI behavior: + +```text +claude auth login --claudeai --email
+claude setup-token +claude auth status --json +``` + +For an already enrolled account, the adapter may provision through: + +```text +CLAUDE_CODE_OAUTH_REFRESH_TOKEN +CLAUDE_CODE_OAUTH_SCOPES +``` + +When those variables are present, `claude auth login` exchanges the existing +refresh credential instead of opening a browser. Because the exchange may +rotate the refresh generation, it requires the current refresh-owner lease and +must publish the resulting complete bundle before releasing the lease. + +Provider CLI versions and observed interface capabilities are recorded as +non-secret transaction metadata. Unsupported or changed prompts fail with +`provider_adapter_incompatible` instead of falling back to undocumented HTTP +calls. + +## Architecture + +```mermaid +flowchart LR + UI["openProfiler React UI"] + Native["openProfiler native backend"] + Broker["Credential broker and job state"] + Provisioner["Privileged mailbox and seat provisioner"] + Worker["Isolated authentication worker"] + CLI["Official Claude CLI"] + Browser["Disposable Playwright context"] + Graph["Scoped Microsoft Graph mailbox reader"] + Vault["Azure Key Vault"] + Lease["Profile registry and lease store"] + Runtime["pclaude runtime"] + + UI -->|"Non-secret enrollment request"| Native + Native -->|"Authorized, idempotent job"| Broker + Broker -.->|"Separate privileged operation"| Provisioner + Broker --> Worker + Worker --> CLI + CLI -->|"Provider-generated authorization URL"| Browser + Browser -->|"Request email login"| Graph + Graph -->|"Single-use link or code"| Browser + Browser -->|"Local callback or returned code"| CLI + CLI -->|"Credential file or setup-token"| Worker + Worker -->|"Validated secret generation"| Broker + Broker -->|"Managed identity"| Vault + Broker --> Lease + Broker -->|"Atomic checkout or process injection"| Runtime + Runtime -->|"Accepted owner rotation"| Broker +``` + +## Trust and Privilege Separation + +### Native openProfiler backend + +The Tauri/Rust backend accepts non-secret requests from the webview, authorizes +the selected company and profile, creates an idempotent reauthentication job, +and displays sanitized state. It never returns credentials, OAuth URLs, magic +links, codes, cookies, message bodies, or credential fingerprints to the +webview. + +### Credential broker + +The broker: + +- authorizes the caller for an immutable profile ID; +- acquires and fences the provider-account refresh lease; +- issues a short-lived worker job capability; +- accepts a complete validated credential generation; +- writes a new exact Key Vault secret version; +- conditionally advances the accepted-generation pointer; and +- emits sanitized audit events. + +The worker does not receive a reusable Key Vault role. It receives only a +transaction-scoped broker capability. + +### Mailbox provisioner + +Mailbox or Entra user creation is a distinct, privileged workflow. It may use a +just-in-time administrative identity to create or license the approved mailbox, +but that identity is never available to the browser or login worker. + +Creating a mailbox is insufficient. The workflow must also establish the +corresponding Claude account, organization membership, subscription or seat, +and Claude Code entitlement through an Anthropic-supported administrative +process such as approved invitation, SSO, or SCIM. + +Shared mailboxes or non-person identities may be used only when the Anthropic +agreement explicitly allows them. Otherwise, enrollment stops before requesting +a Claude login message. + +### Mailbox reader + +The mailbox reader uses a separate Entra service principal or managed identity. +It receives only Microsoft Graph `Mail.Read`, scoped with Exchange Online RBAC +for Applications to the dedicated authentication mailboxes or administrative +unit. It must not also hold an unscoped Entra `Mail.Read` grant, because +independent grants are additive. + +Graph change notifications may wake the transaction, but the worker retrieves +and validates the message through Graph before use. A bounded polling fallback +is allowed when webhook delivery is unavailable. + +### Authentication worker + +The worker runs under a dedicated OS identity or disposable container and owns: + +- a mode-`0700` staging Claude configuration home; +- an isolated Playwright browser context with no persistent user profile; +- a private provider-CLI pseudoterminal; +- a memory-only channel between the CLI, mailbox adapter, browser adapter, and + credential collector; and +- a transaction-scoped broker capability. + +The worker and browser should share a network namespace so the provider's +localhost callback works. If the callback is unavailable, the adapter may use +the CLI's documented returned-code prompt. It must not expose the code through +the UI or clipboard. + +## Domain Model + +### ClaudeEnrollmentTransaction + +```text +transaction_id +request_id +idempotency_key +profile_id +company_connection_id +expected_email +expected_account_id +expected_organization_id +credential_format +provider_cli_version +worker_id +lease_id +lease_epoch +started_at +expires_at +state +human_action_reason +result_code +``` + +No field contains a token, authorization URL, magic link, verification code, +cookie, email body, or raw OAuth response. + +### ClaudeCredentialEnvelope + +```text +schema_version +provider = "claude" +profile_id +credential_format +credential_bundle +expected_account_id +expected_organization_id +provider_cli_version +published_at +source_transaction_id +source_lease_id +source_lease_epoch +generation_id +``` + +`credential_bundle` is secret material and exists only in the native worker, +authenticated transport, broker memory, Key Vault, and an approved local +credential target. The envelope is stored as one indivisible Key Vault secret +version. + +## Transaction State Machine + +```text +requested + -> prerequisites_check + -> waiting_for_lease + -> staging + -> provider_login_started + -> waiting_for_email + -> browser_authorizing + -> collecting_credential + -> validating_identity + -> publishing_generation + -> materializing + -> verifying + -> completed +``` + +Terminal and intervention states are: + +```text +human_action_required +provider_policy_blocked +identity_mismatch +credential_invalid +lease_lost +publication_conflict +expired +cancelled +failed +recovery_required +``` + +A worker may resume only from persisted non-secret job state. It never persists +an authorization URL, magic link, code, cookie, or unaccepted credential to make +a browser transaction resumable. + +## Initial Enrollment Workflow + +1. Resolve the immutable profile, company authority, expected email, and + requested credential format. +2. Confirm the mailbox, Claude account, organization membership, seat, and + Claude Code entitlement. +3. Acquire the provider-account enrollment and refresh lease. +4. Preserve the currently accepted credential generation, if any. +5. Create the restricted staging home and disposable browser context. +6. Remove conflicting authentication variables from the worker environment, + including `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, + `CLAUDE_CODE_OAUTH_TOKEN`, and cloud-provider selection variables. +7. Start the selected official Claude CLI operation in a private + pseudoterminal. +8. Parse the provider-generated authorization URL as secret input and open it + directly in Playwright. +9. Enter only the expected profile email and request the login message. +10. Wait for one matching message through the scoped mailbox reader. +11. Open the verified single-use link in the same Playwright context. If Claude + returns a code, pass it directly to the waiting CLI process. +12. Stop at `human_action_required` for CAPTCHA, MFA, SSO, phone verification, + changed consent, or ambiguous account or organization selection. +13. Collect the native credential file or setup-token without streaming CLI + output to the UI or job logs. +14. Validate structure, scopes, expiry, credential format, live provider status, + account identity, and organization identity. +15. Publish a new exact Key Vault version under the current lease and epoch. +16. Conditionally advance the accepted-generation pointer. +17. Atomically materialize the accepted native credential, or configure + transaction-scoped process injection for an automation token. +18. Perform a minimal provider verification without forcing an unnecessary + refresh. +19. Destroy the staging home and browser context, publish the sanitized result, + and release the lease. + +## Email Correlation and Link Safety + +Magic links and verification codes are authentication factors. The mailbox +adapter must require all of the following: + +- the exact expected recipient mailbox; +- a message received after the transaction's email request time; +- an expected Anthropic sender domain and authentication result; +- an expected subject pattern; +- one outstanding authentication transaction for the mailbox; +- an unexpired transaction window; +- an allow-listed HTTPS destination host after safe URL parsing; and +- a message not already consumed by another transaction. + +The adapter must not select the newest vaguely matching email. It rejects +forwarded messages, replies, unexpected redirect hosts, multiple matches, +expired messages, and links associated with another transaction. + +The raw message body, link, code, and browser response never enter normal logs, +audit records, traces, screenshots, support bundles, or analytics. Consumption +metadata may record only the message ID, transaction ID, timestamps, and +sanitized result. Message deletion or modification requires a separate mailbox +retention policy and is not implicit in credential enrollment. + +## Credential Collection and Validation + +### Native credential bundle + +The collector waits for the staged `.credentials.json` to become stable across +two reads and validates: + +- it is a regular file beneath the staging root and not a symlink; +- the top-level credential schema is recognized; +- access and refresh tokens are non-empty; +- required scopes and expiry metadata are present; +- the subscription and authentication methods match the requested flow; +- the provider reports a logged-in first-party Claude authentication method; +- the expected account and organization identity can be proven; and +- the file remains unchanged throughout collection. + +An access token that currently succeeds does not prove that the refresh token +is usable. The broker must retain refresh ownership and treat the first managed +refresh as a credential-generation transition. + +### Automation token + +`claude setup-token` writes its result to the terminal rather than a credential +file. The native pseudoterminal adapter therefore classifies the matching output +span as secret before any logging, rendering, or telemetry hook receives it. + +The token is stored as `claude_code_oauth_token`, never inside +`.credentials.json`, the profile manifest, `.profile.json`, `settings.json`, a +shell profile, or an environment file. The broker injects it into only the +authorized Claude process as `CLAUDE_CODE_OAUTH_TOKEN`. + +The profile records the one-year expiry and schedules reauthentication before +the warning window. Automation-token expiry does not trigger conversion to a +native credential or use of another profile's refresh token. + +### Identity binding + +Email possession is necessary but not sufficient. Before publication, +openProfiler must prove that the credential belongs to the expected Claude +account and organization. + +The provider adapter should prefer a documented machine-readable Claude +identity response. If the installed CLI returns `null` for email or organization +identity, openProfiler must not infer identity solely from: + +- the email entered in the browser; +- the mailbox that received the magic link; +- a subscription type; +- a browser page label without stable organization identity; or +- successful access to a model or usage endpoint. + +Until Anthropic exposes an adequate supported identity binding, enrollment +requires a human confirmation that displays only non-secret account and +organization information obtained from the authorization UI. A previously +recorded account ID or organization ID mismatch always fails closed. + +## Refresh-Token Provisioning + +Once a complete accepted native credential exists, a new authorized runtime may +be provisioned without email login: + +1. Acquire the refresh-owner lease with a higher fencing epoch. +2. Check out the exact accepted credential generation into worker memory. +3. Set `CLAUDE_CODE_OAUTH_REFRESH_TOKEN` and + `CLAUDE_CODE_OAUTH_SCOPES` only for the isolated `claude auth login` + process. +4. Collect the complete resulting staged credential. +5. Validate identity and credential shape. +6. Publish any rotated generation before activating the runtime. +7. Clear the worker environment and destroy the staging home. + +The old and new refresh generations must never be used concurrently. Losing the +lease during exchange moves the transaction to `recovery_required`; the worker +cannot publish or retry with the previous refresh token. + +## Materialization and Runtime Ownership + +A native credential is written beside the canonical destination, flushed, +assigned mode `0600`, and atomically renamed. The destination must be a regular +file beneath the configured profile root, and no path component may cross an +attacker-controlled symlink. + +The activated Claude runtime owns refresh rights only while its lease is active. +openProfiler watches the parent credential directory because Claude may replace +the credential file atomically. A changed file is accepted only after stable +reads, complete validation, identity equality, and lease fencing. Each accepted +rotation becomes a new Key Vault version and advances the accepted-generation +pointer conditionally. + +An automation token is injected into one authorized process. It is not copied to +other profile homes, and it does not participate in native refresh-token file +watching. + +## Human and Administrative Gates + +The transaction stops without automatic retries when it encounters: + +- CAPTCHA or bot-detection challenge; +- MFA, SSO, phone verification, or conditional-access interaction; +- changed provider consent; +- more than one selectable account or organization; +- unexpected account, organization, subscription, or seat; +- OAuth disabled by the Claude organization; +- Claude Code disabled or no eligible seat; +- mailbox quarantine or message-delivery policy failure; +- a link or code that cannot be bound to the transaction; +- provider CLI interface incompatibility; or +- an existing refresh owner that cannot be safely stopped or transferred. + +The UI reports the clearing actor and action, such as `Claude organization +administrator must enable OAuth`, `Exchange administrator must release the +message from quarantine`, or `authorized user must complete MFA`. It never +offers to bypass the control. + +## Failure and Recovery + +- A failed or cancelled login leaves the previous accepted generation intact. +- An incomplete or invalid staged credential is destroyed and never published. +- A Key Vault version written before a failed pointer update is an unaccepted + orphan and is never restored automatically. +- A provider-policy `403` becomes `provider_policy_blocked`, not a retry loop. +- A revoked or expired credential becomes `reauthentication_required`. +- Multiple matching email messages become `human_action_required`. +- A lost lease or uncertain refresh generation becomes `recovery_required`. +- An unmanaged runtime that may refresh prevents ownership transfer. +- Cleanup failure seals the staging directory and raises a security alert; it + does not expose the directory path or contents in the webview. + +## Broker API Additions + +The existing proposed broker surface includes profile reauthentication. The +Claude adapter requires job-oriented status and human continuation: + +```text +POST /v1/profiles/{profile-id}/reauthentication +GET /v1/profiles/{profile-id}/reauthentication/{transaction-id} +POST /v1/profiles/{profile-id}/reauthentication/{transaction-id}/continue +POST /v1/profiles/{profile-id}/reauthentication/{transaction-id}/cancel +``` + +Every mutation requires an idempotency key, caller authorization, current lease +where applicable, and transaction expiry. `continue` accepts only a signed +human-action acknowledgment or non-secret organization selection identifier. It +never accepts a token, link, code, cookie, email body, or credential file from +the webview. + +## Audit Events + +Sanitized event types include: + +```text +claude_enrollment_requested +claude_prerequisites_satisfied +claude_provider_login_started +claude_email_received +claude_human_action_required +claude_credential_validated +claude_identity_mismatch +claude_provider_policy_blocked +claude_generation_published +claude_profile_materialized +claude_enrollment_completed +claude_enrollment_failed +``` + +Events contain transaction ID, profile ID, company authority, worker ID, lease +epoch, timestamps, result, and sanitized error code. They never contain token +values or fingerprints, OAuth URLs, magic links, verification codes, cookies, +email bodies, raw provider output, or raw JWT claims. + +## Acceptance Criteria + +1. A profile can complete authorized native Claude enrollment without secret + material entering the React webview, logs, telemetry, screenshots, or crash + reports. +2. An automation token can be generated, stored as an exact Key Vault version, + and injected without being written to a profile or settings file. +3. A failed or cancelled enrollment leaves the prior accepted credential + unchanged. +4. The login worker cannot read mail outside the dedicated authentication + mailbox scope. +5. The login worker has no mailbox-provisioning or Exchange administrative + role. +6. Magic-link selection rejects messages outside the active transaction window, + unexpected senders or hosts, multiple matches, and already-consumed + messages. +7. CAPTCHA, MFA, SSO, phone verification, consent, identity ambiguity, and + organization policy failures require human or administrative action. +8. A credential cannot be published unless expected account and organization + identity are proven. +9. Refresh-token provisioning requires an active fenced lease and publishes any + rotated complete generation. +10. Two runtimes cannot refresh one account concurrently. +11. Every accepted credential is stored as one indivisible exact Key Vault + version and atomically materialized with restricted permissions. +12. Tests inject token-like canaries into every secret input and prove they are + absent from frontend payloads, logs, audit events, errors, traces, crash + reports, and support bundles. +13. The feature is disabled unless the deployment records the applicable + Anthropic authorization and allowed account types. + +## Delivery Plan + +### Phase 1: Authorization and provider contract + +- Obtain written Anthropic confirmation for the intended Team or Enterprise + deployment. +- Choose supported account and seat provisioning. +- Define the authoritative account and organization identity binding. +- Record supported Claude CLI versions and credential formats. + +### Phase 2: Isolated provider adapter + +- Add staging-home lifecycle and private pseudoterminal handling. +- Implement native credential collection and setup-token secret capture. +- Add environment sanitization and provider compatibility detection. +- Add structural validation and sanitized failure codes. + +### Phase 3: Scoped mailbox integration + +- Register a dedicated mailbox-reader service principal. +- Configure Exchange Online Application RBAC for the authentication mailbox + scope. +- Add Graph change notifications with bounded polling fallback. +- Implement strict message and link correlation. + +### Phase 4: Playwright authorization worker + +- Co-locate CLI and disposable browser context. +- Support provider callback and documented returned-code completion. +- Add domain allowlisting and direct in-memory secret transfer. +- Add explicit human-action transitions. + +### Phase 5: Broker publication and materialization + +- Publish exact Key Vault versions under lease fencing. +- Add accepted-generation conditional updates. +- Atomically materialize native credentials. +- Inject automation tokens into authorized processes only. + +### Phase 6: Refresh provisioning and runtime watching + +- Add supported refresh-token provisioning. +- Integrate `pclaude` with distributed refresh leases. +- Publish accepted runtime rotations. +- Add expiry warnings, reauthentication scheduling, revoke, and recovery. + +## Open Questions + +- Does the applicable Anthropic agreement permit mailbox- and + Playwright-assisted enrollment for these company-owned Team or Enterprise + accounts? +- Which Anthropic-supported mechanism creates accounts, assigns seats, and + enables Claude Code: invitation, SSO, SCIM, or another administrative API? +- What documented provider response can bind a Claude credential to an immutable + account and organization ID when `claude auth status --json` omits them? +- Are shared or non-person mailboxes permitted account identities? +- Should the first release support only automation tokens, only native profile + credentials, or both? +- Which worker platform provides the required browser isolation and local + callback behavior across Windows, WSL, and containers? +- What renewal window and human escalation SLA apply to one-year automation + tokens and expiring native logins? + +## References + +- [Claude Code authentication](https://code.claude.com/docs/en/authentication) +- [Claude Code environment variables](https://code.claude.com/docs/en/env-vars) +- [Claude Code legal and compliance](https://code.claude.com/docs/en/legal-and-compliance) +- [Log in to a Claude account](https://support.claude.com/en/articles/13189465-log-in-to-your-claude-account) +- [Exchange Online RBAC for Applications](https://learn.microsoft.com/exchange/permissions-exo/application-rbac) +- [Microsoft Graph Outlook change notifications](https://learn.microsoft.com/graph/outlook-change-notifications-overview) +- [OAuth 2.0 Security Best Current Practice](https://www.rfc-editor.org/rfc/rfc9700.html) diff --git a/docs/cloud-credential-vault-feature.md b/docs/cloud-credential-vault-feature.md new file mode 100644 index 0000000..e1171b0 --- /dev/null +++ b/docs/cloud-credential-vault-feature.md @@ -0,0 +1,574 @@ +# Cloud Credential Vault and Broker + +Status: proposed +Target: openProfiler after `0.1.4` +Scope: Claude and Codex credentials across Windows, WSL, containers, and +multiple workstations + +## Summary + +openProfiler should provide a remote credential authority for provider +profiles. Complete provider credential bundles are stored as versioned Azure +Key Vault secrets and checked out through an Entra-authenticated credential +broker. + +The broker coordinates one refresh owner per provider account. This prevents +Windows, WSL, containers, or multiple workstations from independently rotating +copies of the same refresh token and invalidating each other. + +Key Vault is the durable accepted-generation store. It is not a license to run +the same refresh credential concurrently in multiple runtimes. + +## User Problem + +Provider credentials currently live in workstation profile files: + +```text +Claude: ~/.claude-profiles/profiles//.credentials.json +Codex: ~/.chatgpt-profiles/profiles//auth.json +``` + +This creates four failure modes: + +1. A provider logout or interrupted login can remove the only local + credential. +2. A refreshed credential on Windows can leave WSL and containers with a + revoked refresh-token generation. +3. Moving to a new workstation requires repeating provider authentication. +4. Copying credentials to avoid reauthentication creates multiple refresh + writers and makes revocation races more likely. + +openProfiler needs durable recovery without turning credential copies into +concurrent authorities. + +## Goals + +- Store each accepted complete credential bundle as a versioned Key Vault + secret. +- Restore a still-valid credential on a new workstation after Entra + authorization. +- Coordinate refresh ownership across Windows, WSL, containers, and + workstations. +- Publish every accepted provider refresh as a new credential generation. +- Support company-owned and personal profile authorities without mixing their + trust boundaries. +- Keep all credential contents out of the React webview, logs, analytics, + crash reports, shell output, and source control. +- Preserve older Key Vault versions for bounded recovery and audit. +- Provide explicit deactivate, reauthenticate, and revoke operations. + +## Non-Goals + +- Key Vault cannot make a provider-revoked refresh token valid again. +- The feature does not permit simultaneous use of one rotating refresh token + by multiple owners. +- openProfiler does not become the provider's OAuth authorization server. +- The browser UI does not receive or directly manage provider tokens. +- An email domain is not sufficient evidence of an Entra tenant or company + authority. +- The feature does not silently upload existing credentials during profile + discovery. + +## Architecture + +```mermaid +flowchart LR + UI["openProfiler React UI"] + Native["openProfiler native backend"] + Entra["Microsoft Entra ID"] + Broker["Credential broker API"] + Lease["Distributed lease and metadata store"] + Vault["Azure Key Vault"] + Runtime["Claude or Codex runtime"] + + UI -->|"non-secret commands and status"| Native + Native -->|"authorization code with PKCE"| Entra + Native -->|"Entra access token"| Broker + Broker -->|"managed identity"| Vault + Broker --> Lease + Native -->|"atomic provider file"| Runtime + Runtime -->|"rotated provider file"| Native + Native -->|"accepted owner publication"| Broker +``` + +### React webview + +The webview displays: + +- profile and company names; +- declared provider identity; +- credential readiness; +- current refresh owner; +- checkout and synchronization status; +- recovery-required state; +- sanitized audit events. + +The webview never receives a credential bundle, token, token fingerprint, raw +JWT claim set, Key Vault secret value, or Entra token. + +### Native backend + +The Tauri/Rust backend: + +- authenticates the user with Entra using authorization code with PKCE; +- holds Entra tokens in an operating-system-protected token cache; +- calls the credential broker; +- validates provider credential shape and expected identity; +- atomically materializes credentials into approved provider profile paths; +- watches the owning runtime for atomic credential replacement; +- publishes accepted rotations; +- deletes or seals dormant checkouts according to local policy. + +Credential bytes may exist only in native process memory, restricted temporary +files, the approved provider credential file, encrypted transport, and Key +Vault. + +### Credential broker + +The broker is the only service allowed to: + +- authorize a user for a company and profile; +- issue and fence refresh-owner leases; +- read an exact Key Vault secret version; +- accept a new credential generation from the current lease owner; +- validate provider, profile, account, and generation metadata; +- advance the accepted-generation pointer; +- mark a profile conflicted, revoked, or recovery-required; +- emit sanitized audit events. + +Clients do not receive direct Key Vault data-plane permissions. + +### Azure Key Vault + +Use a dedicated vault boundary per company, application, and environment. A +company deployment owns its vault, broker identity, authorization groups, and +retention policy. + +Recommended controls: + +- Azure RBAC data-plane authorization; +- broker managed identity with narrowly scoped secret read and write + permissions; +- no standing secret-read role for workstation users; +- Privileged Identity Management for administrative access; +- soft delete and purge protection; +- private broker-to-vault network access where practical; +- Key Vault diagnostics sent to the company's security workspace; +- no emails or other personal data in secret names. + +Personal profiles use a separately configured personal authority. Company +credentials must not be stored in another company's vault merely because the +same user operates both profiles. + +## Company Connection + +openProfiler records non-secret connection metadata: + +```text +company_connection_id +display_name +entra_tenant_id +broker_url +broker_audience +authority_id +profile_registry_version +status +``` + +The initial company enrollment flow asks the user to select or enter the +company and then authenticates against its configured Entra tenant. It does +not discover an identity provider by searching the user's email address. + +The broker maps the Entra tenant, subject, groups, and application roles to +authorized profile IDs. + +## Credential Storage Model + +### Secret naming + +Use opaque immutable profile IDs: + +```text +provider-credential--claude-- +provider-credential--codex-- +``` + +Profile aliases and emails remain in the authorized profile registry, not in +secret names. + +### Secret value + +Each Key Vault secret version contains one complete credential envelope: + +```text +schema_version +provider +profile_id +credential_format +credential_bundle +published_at +source_lease_id +source_lease_epoch +generation_id +``` + +`credential_bundle` contains the complete native provider file. openProfiler +must never combine token fields from different generations. + +### Authority metadata + +Non-secret authority metadata is stored separately from Key Vault: + +```text +profile_id +provider +expected_email +expected_account_id +accepted_generation_id +accepted_secret_version_uri +accepted_at +state +lease_owner +lease_epoch +lease_expires_at +``` + +The accepted secret is always addressed by exact version URI. A client must +not interpret the unnamed latest Key Vault version as authoritative. + +Credential hashes may be used internally for equality checks, but are treated +as private operational metadata and are not displayed or logged. + +## Distributed Refresh Lease + +Each provider account has at most one active refresh owner: + +```text +none +windows: +wsl:: +container:: +``` + +A lease contains: + +```text +lease_id +profile_id +account_id +owner +epoch +issued_at +heartbeat_at +expires_at +state +``` + +The monotonically increasing epoch is a fencing token. The broker rejects a +publication from an expired owner even if that owner still possesses an old +lease ID. + +Key Vault stores credential generations but is not the lock service. The +broker uses a distributed metadata store with conditional writes or leases to +coordinate ownership and the accepted-version pointer. + +## Broker API + +The initial API surface is narrow: + +```text +GET /v1/profiles +GET /v1/profiles/{profile-id}/status +POST /v1/profiles/{profile-id}/leases +POST /v1/profiles/{profile-id}/leases/{lease-id}/heartbeat +POST /v1/profiles/{profile-id}/checkout +POST /v1/profiles/{profile-id}/publish +POST /v1/profiles/{profile-id}/leases/{lease-id}/release +POST /v1/profiles/{profile-id}/reauthentication +POST /v1/profiles/{profile-id}/revoke +``` + +All mutating requests use request IDs and idempotency keys. Checkout and +publication require the current lease ID and epoch. Credential payloads travel +only between the native backend and broker over authenticated TLS and are +excluded from request logging and telemetry. + +## Core Workflows + +### Enroll an existing profile + +1. Discover the local profile without reading credential contents into the + webview. +2. Ask the user to select the owning personal or company authority. +3. Authenticate the user to that authority through Entra. +4. Acquire an enrollment transaction. +5. Validate the local provider file and expected account identity. +6. Upload it as the first Key Vault secret version. +7. Record the exact accepted version and non-secret profile metadata. +8. Verify a broker checkout and byte-for-byte native round trip. +9. Mark the profile `vault_backed`. + +Enrollment is explicit and never part of passive profile discovery. + +### Start a provider runtime + +1. Resolve the identity profile and company connection. +2. Authenticate the user and authorize the profile. +3. Acquire the distributed refresh lease. +4. Checkout the exact accepted Key Vault generation. +5. Validate provider type, account identity, lease, and generation. +6. Atomically install the complete native credential file with restricted + permissions. +7. Launch the provider runtime. +8. Heartbeat the lease and watch the parent credential directory. +9. Publish accepted rotations from the owning runtime. +10. Check in the final stable generation and release the lease on exit. + +If another device owns the lease, openProfiler blocks startup and identifies +the owner and last heartbeat without exposing credential details. + +### Publish a refresh + +1. Detect that the owning runtime replaced its credential file. +2. Wait for two stable reads with the same hash. +3. Validate the native structure and expected account identity. +4. Send the complete bundle with the lease ID, epoch, and previous generation. +5. Have the broker verify current ownership. +6. Write a new Key Vault secret version. +7. Conditionally advance the accepted-version pointer. +8. Mark non-owning checkouts stale. + +A Key Vault version written before a failed pointer update is an unaccepted +orphan and must never be restored automatically. + +### Transfer ownership + +1. Ask the current owner to publish its final stable generation. +2. Stop or otherwise prove the current runtime cannot refresh. +3. Release its lease. +4. Acquire a new lease with a higher epoch. +5. Checkout the exact accepted generation into the new owner. +6. Start and verify the new runtime. + +The broker must not overwrite a credential file belonging to a running +non-owner. + +### Reauthenticate safely + +Reauthentication uses an isolated staging home instead of the canonical +profile directory: + +1. Preserve the accepted Key Vault generation. +2. Create a restricted temporary Claude or Codex configuration home. +3. Start the provider's supported login flow in that staging home. +4. Wait for provider completion. +5. Validate the resulting complete credential and expected account identity. +6. Publish it as a new accepted Key Vault generation. +7. Atomically install the accepted generation into the canonical local profile. +8. Destroy the staging home. + +An interrupted login therefore cannot erase the last accepted local or remote +credential. If the provider explicitly revoked the previous refresh token, +that older version remains retained for audit but is not considered usable. + +### Deactivate, reauthenticate, and revoke + +These are separate user operations: + +- **Deactivate this device:** publish the final owner generation, release the + lease, and remove or seal the local checkout. Preserve Key Vault. +- **Sign in again:** use the staged reauthentication workflow and publish a new + accepted generation. +- **Revoke everywhere:** invoke the provider's supported logout or revocation + flow, release all ownership, and mark the authority record revoked. Retain or + disable historical Key Vault versions according to company policy. + +Deletion of a local credential by an unmanaged provider process is not a +request to delete Key Vault data. openProfiler marks the checkout +`recovery_required` and requires an explicit restore or reauthentication +decision. + +### Restore on a new workstation + +1. Install openProfiler without provider credentials. +2. Add the company connection using its known tenant and broker information. +3. Authenticate with Entra. +4. Discover the user's authorized profiles from the broker. +5. Acquire a lease for the selected profile. +6. Checkout and validate the accepted generation. +7. Materialize the provider profile structure and credential atomically. +8. Verify provider account status. + +Restore can avoid provider authentication only while the accepted refresh +credential remains valid. + +## Conflict and Failure Handling + +The broker enters `conflict` or `recovery_required` when: + +- two runtimes changed credentials without a valid owner; +- an expired lease owner attempts publication; +- the credential account does not match the profile; +- the provider file changes during snapshot; +- the accepted-version pointer and Key Vault generation disagree; +- a restored refresh token is rejected; +- a broker restart cannot establish one current owner. + +Resolution stops all writers and prefers the final generation from the +recorded lease owner. The broker never repeatedly tests competing refresh +tokens because one refresh attempt may invalidate another candidate. + +If ownership or identity cannot be proven, the required recovery is a fresh +staged provider login. + +## Local Storage + +Provider runtimes still require native credential files. openProfiler writes +those files atomically and restricts access: + +- Unix files use mode `0600`; +- Windows files preserve a user-only ACL; +- source and target paths must be regular files beneath approved roots; +- symlinks and path traversal are rejected; +- temporary files are created beside the destination, flushed, renamed + atomically, and removed on failure. + +An optional operating-system-sealed cache may support read-only diagnostics +while offline. It never grants refresh ownership without a broker lease. + +## SOPS Relationship + +The existing SOPS registry becomes optional cold disaster-recovery escrow. It +is not the active synchronization channel, accepted-generation pointer, or +lease authority. + +SOPS restoration must not overwrite a newer accepted Key Vault generation. +Because old refresh tokens may have been rotated or revoked, restored SOPS +material always enters `recovery_required` until validated by a controlled +owner. + +## Security Requirements + +- Use Entra authorization code with PKCE for the native application. +- Authorize company and profile access in the broker, not only in the client. +- Give workstations no direct Key Vault data-plane role. +- Use managed identity between the broker and Key Vault. +- Apply least-privilege Azure RBAC and just-in-time administrator access. +- Enable Key Vault soft delete, purge protection, diagnostics, and alerts. +- Never put provider credentials, Entra tokens, raw JWT claims, or credential + hashes in normal logs or audit events. +- Redact HTTP bodies before broker telemetry is initialized. +- Keep secrets out of React state, Tauri events, clipboard operations, crash + reports, and support bundles. +- Require account-ID equality; an email match alone is insufficient once an + account ID has been enrolled. +- Preserve a bounded sanitized audit trail of lease, checkout, publish, + transfer, reauthentication, and revocation events. + +## Product States + +Each profile displays one of: + +```text +local_only +enrollment_available +vault_backed +checked_out_here +active_here +active_elsewhere +stale_checkout +reauthentication_required +conflict +revoked +unavailable +``` + +The UI displays the current owner device, last successful synchronization, and +required action. It never claims that a token is valid based only on Key Vault +presence. + +## Acceptance Criteria + +1. A user can enroll a Claude or Codex profile without credential material + entering the webview or logs. +2. A fresh workstation can restore a valid enrolled profile after Entra + authorization. +3. Windows and WSL cannot simultaneously own refresh rights for the same + account. +4. A stale or expired owner cannot publish after a lease transfer. +5. A provider refresh creates a new Key Vault version and atomically advances + the accepted-version pointer. +6. A failed or cancelled reauthentication leaves the prior accepted + generation intact. +7. Local credential deletion does not delete or replace the Key Vault + authority. +8. Account mismatches fail closed before local activation or remote + publication. +9. Provider logout is distinct from local deactivation and cannot be triggered + accidentally by removing a checkout. +10. Automated tests verify that credentials are absent from frontend payloads, + logs, audit records, errors, and crash diagnostics. +11. Company profiles cannot be restored through another company's authority. +12. SOPS or an older Key Vault version cannot silently overwrite a newer + accepted generation. + +## Delivery Plan + +### Phase 1: Authority inventory + +- Add company connections and non-secret remote profile metadata. +- Display local-only and vault-backed state. +- Detect duplicate accounts and unmanaged concurrent writers. +- Make no remote credential changes. + +### Phase 2: Broker and vault enrollment + +- Deploy one company broker, managed identity, metadata store, and dedicated + Key Vault. +- Implement explicit enrollment, exact-version checkout, and audit. +- Support manual restore with no automatic runtime launch. + +### Phase 3: Distributed leases + +- Implement lease acquisition, heartbeat, epoch fencing, release, and crash + recovery. +- Integrate Windows and WSL ownership transfer. +- Block unmanaged concurrent activation where detectable. + +### Phase 4: Provider adapters + +- Add complete-bundle validation for Claude and Codex. +- Add stable file watchers and accepted rotation publication. +- Integrate `pclaude` and `pcodex` with broker leases. + +### Phase 5: Staged reauthentication + +- Add isolated provider login homes. +- Add explicit deactivate, sign-in-again, and revoke workflows. +- Add rollback and recovery-required handling. + +### Phase 6: Multi-company and migration + +- Add company-specific Entra authorities and broker discovery. +- Migrate explicitly selected SOPS and local profiles. +- Retain SOPS only under documented cold-escrow policy. + +## Dependencies + +- Microsoft Entra application registration for the native openProfiler client; +- company credential broker application and API scope; +- broker managed identity; +- dedicated Azure Key Vault with Azure RBAC; +- distributed metadata and lease store; +- provider-specific credential validators; +- native OS token-cache integration; +- `pclaude` and `pcodex` broker-aware launchers. + +## Related Documents + +- [Claude OAuth enrollment and automated reauthentication](claude-oauth-enrollment-automation.md) +- [Codex credential rotation and synchronization architecture](codex-token-management-architecture.md) +- [Codex identity and history profile architecture](codex-history-profile-architecture.md) +- [Azure Key Vault security guidance](https://learn.microsoft.com/azure/key-vault/general/secure-key-vault) +- [Azure Key Vault authentication](https://learn.microsoft.com/azure/key-vault/general/authentication) +- [OAuth 2.0 Security Best Current Practice](https://www.rfc-editor.org/rfc/rfc9700.html) diff --git a/docs/codex-history-profile-architecture.md b/docs/codex-history-profile-architecture.md new file mode 100644 index 0000000..70b75a2 --- /dev/null +++ b/docs/codex-history-profile-architecture.md @@ -0,0 +1,653 @@ +# Codex Identity and History Profile Architecture + +Status: proposed +Target: openProfiler after `0.1.4` +Scope: local Codex profiles and local Codex chat history + +## Purpose + +openProfiler currently discovers provider identities and activates their local +credentials. The next capability should let a user: + +1. browse Codex identity profiles independently from Codex history profiles; +2. inspect the chats stored in each history profile; +3. copy or move a chat between history profiles; +4. resume a selected chat with a selected identity; +5. hand a selected identity and chat to the Windows ChatGPT/Codex app when the + desktop app can safely consume them. + +Identity and history are separate product concepts, but Codex currently stores +authentication, sessions, configuration, and runtime state beneath one +`CODEX_HOME`. openProfiler must therefore materialize an identity/history +selection into an isolated runtime binding. It must not imply that Codex +natively supports independently selectable credential and history roots. + +## Architecture Decisions + +### Use Codex app-server as the integration boundary + +openProfiler must use the documented Codex app-server JSON-RPC API for account +state and chat history: + +- `account/read`, `account/login/start`, and `account/logout`; +- `thread/list`, `thread/read`, `thread/resume`, and `thread/fork`; +- `thread/archive` and `thread/unarchive`. + +The app-server is the supported interface for rich clients that need +authentication, conversation history, approvals, and streamed agent events. +openProfiler should generate TypeScript or JSON schemas from the installed +Codex version during development and pin generated clients to that version: + +```bash +codex app-server generate-ts --out ./schemas +codex app-server generate-json-schema --out ./schemas +``` + +SQLite is an implementation detail. openProfiler must not query, migrate, copy, +or update Codex SQLite tables. The schema is not a stable public contract. + +### Treat rollout JSONL as an opaque transfer artifact + +Codex persists an active thread as an append-only JSONL file beneath: + +```text +$CODEX_HOME/sessions/YYYY/MM/DD/rollout--.jsonl +``` + +Archived threads are stored beneath `$CODEX_HOME/archived_sessions`. Codex +app-server can scan these files and repair its state database when +`thread/list` is called without `useStateDbOnly`. + +There is no documented Codex-to-Codex thread export/import API. Until one +exists, openProfiler may copy a rollout file between history profiles only as +an opaque file: + +- never rewrite events or identifiers; +- calculate and retain a SHA-256 before and after transfer; +- validate the destination through `thread/read`; +- keep the source until destination validation succeeds; +- avoid transferring a thread while either side can write to it. + +`history.jsonl` is not a substitute for a rollout. It is lightweight prompt +history and is insufficient for a complete resumable thread. + +### Separate logical profiles and physical runtime bindings + +The user selects an `IdentityProfile` and a `HistoryProfile`. openProfiler +creates or activates a `RuntimeBinding` that presents the selected values to +one Codex app-server process. + +```text +IdentityProfile ─┐ + ├─> RuntimeBinding -> Codex app-server +HistoryProfile ──┘ | + | └─> selected credential + └──────────────────────> physical CODEX_HOME +``` + +A runtime binding has one active identity and one writable history. The same +history profile must not have multiple writable bindings at the same time. + +Codex does not expose an independent sessions-root setting. The history profile +therefore owns the binding's physical `CODEX_HOME`, including `sessions` and +`archived_sessions`. openProfiler atomically materializes the selected +identity's credential into that home before app-server starts and writes a +refreshed credential back to the identity profile when it stops. The runtime +credential is an active credential copy, not a second credential registry. + +### Distinguish managed and external histories + +History discovered in an existing Codex home is initially external and +read-only. openProfiler does not control other CLI or desktop processes that +may write there, so its own lock cannot provide exclusive access. + +A history becomes writable when either: + +- openProfiler created and owns its managed history home; or +- the user explicitly adopts an existing history while all other Codex writers + are stopped. + +Adoption writes non-secret ownership metadata and enables openProfiler's +single-writer lease. Commands launched outside openProfiler do not honor that +lease, so mutating operations still require a best-effort process/activity +check. The Windows app home always uses the separate desktop handoff workflow. + +### Keep managed ChatGPT login as the default + +The first implementation should continue using complete file-based ChatGPT +OAuth credentials and Codex-managed refresh. Before a runtime binding stops, +openProfiler saves a refreshed credential back to the matching identity +profile using the same identity verification and atomic replacement rules used +by the existing Windows desktop switch. + +Codex app-server also supports experimental `chatgptAuthTokens`, where a host +application supplies access tokens and answers refresh requests. This would +decouple authentication from `CODEX_HOME`, but it makes openProfiler responsible +for the OAuth lifecycle and token refresh timing. It must remain disabled +behind an explicit experimental feature until the managed runtime design is +proven. + +Credential checkout, refresh ownership, Windows/WSL synchronization, conflict +handling, and SOPS recovery policy are defined separately in +[Codex credential rotation and synchronization architecture](codex-token-management-architecture.md). + +### Do not hot-switch the running Windows app + +The Windows ChatGPT/Codex app uses its Windows Codex home and exposes no +documented API for hot-switching to an arbitrary `CODEX_HOME`. openProfiler +must not overwrite its session files or credentials while the app is running +as a non-owner. Reading and publishing a stable credential snapshot from the +running Windows owner is allowed; transferring refresh ownership requires +quiescing the Windows app. + +The initial desktop integration is a controlled handoff of one identity and +one thread into the fixed desktop home. It is not a native desktop history +profile switch. + +## Domain Model + +### IdentityProfile + +An existing provider login that can be activated. + +```text +id +provider +name +email +family +aliases +profile_path +credential_kind +credential_present +credential_store +account_id +workspace_id +plan_type +source +``` + +Credential paths, tokens, and refresh material remain backend-only. The +webview receives non-secret readiness and account metadata. + +### HistoryProfile + +A named collection of Codex sessions. + +```text +id +provider +name +family +codex_home +sessions_root +archived_sessions_root +sqlite_home +canonical_root +source +ownership +access_mode +readiness +thread_count +last_activity_at +``` + +History profiles can be discovered from: + +1. explicit openProfiler history metadata; +2. canonicalized `sessions` paths referenced by existing Codex profiles; +3. compatible workBenches family state such as + `~/.chatgpt-profiles/state/opensoft/sessions`; +4. the active Windows or Unix Codex home. + +Profiles that resolve to the same canonical sessions root are one history +profile even when several identity profiles reference it. + +`ownership` is `managed` or `external`. `access_mode` is `read_only`, +`writable`, or `busy`. Discovery never silently promotes an external history to +writable. + +For compatible workBenches family state, the family state directory can become +the history profile's `codex_home`. For example, +`~/.chatgpt-profiles/state/opensoft` owns its `sessions` and +`archived_sessions` directories. A sessions-only source that cannot safely act +as a Codex home must be imported into a managed history home before it becomes +writable; openProfiler must not construct a writable runtime from undocumented +session-path overrides. + +### RuntimeBinding + +The active pairing used by an app-server process. + +```text +id +identity_profile_id +history_profile_id +codex_home +sqlite_home +codex_version +app_server_pid +status +lease +started_at +last_verified_at +``` + +Statuses should include `inactive`, `starting`, `ready`, `busy`, `stopping`, +`failed`, and `recovery_required`. + +### ThreadRecord + +A UI projection returned by app-server, not a parsed SQLite row. + +```text +thread_id +session_id +history_profile_id +name +preview +cwd +model_provider +created_at +updated_at +status +archived +forked_from_id +rollout_path +rollout_sha256 +``` + +The rollout path and hash remain backend metadata. Chat bodies are loaded on +demand with `thread/read(includeTurns: true)`. + +### TransferOperation + +A journaled copy or move between history profiles. + +```text +id +mode +thread_id +source_history_profile_id +destination_history_profile_id +source_path +destination_path +source_sha256 +destination_sha256 +state +started_at +completed_at +error +``` + +States should include `planned`, `source_locked`, `copied`, `validated`, +`committed`, `rolled_back`, and `failed`. + +### DesktopHandoff + +The recoverable state for a thread checked out to the Windows app. + +```text +id +identity_profile_id +history_profile_id +thread_id +source_sha256 +desktop_sha256 +credential_rollback_available +state +started_at +returned_at +``` + +## Storage Layout + +openProfiler should add a non-secret registry and runtime area without creating +a second credential database: + +```text +~/.config/openprofiler/ + history-profiles.json + +~/.local/share/openprofiler/ + histories/ + / + codex-home/ + sessions/ + archived_sessions/ + +~/.local/state/openprofiler/ + runtimes/ + / + binding.json + histories/ + / + sqlite/ + transfers/ + .json + locks/ + history-.lock + desktop-handoffs/ + .json +``` + +On Windows, use the corresponding per-user configuration and local-state +directories. Credential material is activated from the existing provider +profile store and is never written into `history-profiles.json`, transfer +journals, logs, or frontend state. + +An explicit history registry entry should contain only a stable ID, display +metadata, and a path relative to an approved profile root where possible. +Absolute external paths require explicit user approval and the same +canonicalization checks used for identity profile discovery. + +At runtime, the process environment is assembled as: + +```text +CODEX_HOME= +CODEX_SQLITE_HOME= +/auth.json= +``` + +Existing compatible history homes remain in place. The managed +`~/.local/share/openprofiler/histories` area is for imported or newly created +history profiles, not for duplicating every discovered history. + +Managed history homes contain no durable identity registry. Their activated +`auth.json` is removed after refreshed credentials are written back on a clean +shutdown. Recovery treats a credential left by an interrupted runtime as +sensitive pending state: verify its non-secret identity, write it back to the +matching identity profile, then remove it before activating another identity. + +## Components + +### Rust core + +The current `open-profiler-core` crate should grow provider-neutral domain +types with Codex-specific adapters: + +```text +identity/ + inventory + activation + +history/ + inventory + registry + transfer + locking + +codex/ + app_server + generated_protocol + runtime + +desktop/ + codex_handoff +``` + +The core owns path validation, process-independent locks, transfer journals, +atomic writes, hashes, identity checks, and recovery. It must remain usable +without Tauri. + +### Tauri backend + +Add narrow commands rather than exposing generic filesystem or process access: + +```text +history_profiles +history_threads +history_thread +copy_history_thread +move_history_thread +start_codex_runtime +stop_codex_runtime +resume_codex_thread +fork_codex_thread +desktop_handoff_start +desktop_handoff_return +desktop_handoff_rollback +``` + +Long operations emit typed progress events. Commands return sanitized domain +objects and structured error codes. + +### React frontend + +Add three work-focused views: + +1. **Profiles**: existing identity inventory plus associated history profile. +2. **History**: history-profile selector, searchable thread list, thread + details, copy, move, archive, and resume actions. +3. **Runtime**: selected identity, selected history, verified account, + app-server state, active thread, and handoff state. + +The identity selector and history selector remain visibly independent. Before +starting a runtime, show the resulting binding as: + +```text +Use with +``` + +Chat contents should not be loaded for list rows. Load them only when a user +opens thread details. + +## Core Workflows + +### Inventory histories + +1. Discover Codex identity profiles using the existing inventory. +2. Resolve each profile's effective sessions and archived-sessions paths. +3. Canonicalize and group matching roots. +4. Merge explicit history metadata. +5. Mark externally owned roots read-only unless they have been explicitly + adopted. +6. Start a read-only app-server for the selected history runtime. +7. Call `thread/list` with `useStateDbOnly` omitted. +8. Return sanitized thread summaries to the frontend. + +Inventory failures for one history profile must not hide other valid profiles. + +### Resume with a selected identity + +1. Acquire the history profile's single-writer lease. +2. Create or recover the runtime binding. +3. Resolve the history profile's approved Codex home and SQLite state root. +4. Atomically activate the selected complete credential into that Codex home. +5. Start `codex app-server` with history-specific `CODEX_HOME` and + `CODEX_SQLITE_HOME`. +6. Call `account/read` and verify the expected non-secret account identity. +7. Call `thread/read` for the selected thread. +8. Call `thread/resume`. +9. Stream activity through the app-server protocol. +10. On shutdown, wait for idle state, stop app-server, persist refreshed + credentials to the matching identity profile, remove the runtime + credential from managed history homes, and release the lease. + +Failure to verify the account stops the runtime before any model request. + +### Copy a thread + +1. Confirm source and destination differ. +2. Acquire deterministic source and destination locks. +3. Verify the source thread is not active. +4. Resolve the rollout path from trusted backend inventory. +5. Hash the source and create a transfer journal. +6. Copy to a mode-restricted temporary destination. +7. Flush, hash, and atomically rename the destination. +8. Start or refresh destination app-server inventory. +9. Validate the thread with `thread/read`. +10. Commit the journal and release locks. + +The source remains unchanged. + +### Move a thread + +Run the copy workflow first. After destination validation, archive the source by +default. Permanent deletion is a separate destructive action because Codex +deletion can also affect descendant sessions. + +When a thread has known descendants, the UI must offer: + +- copy only this thread; +- copy the thread tree; +- cancel. + +The first implementation may reject tree moves until parent/descendant +behavior is covered by integration tests. + +### Windows desktop handoff + +This workflow is experimental and Windows-only: + +1. Verify the selected identity has a complete reusable file credential. +2. Verify no previous handoff or credential rollback is unresolved. +3. Ask the Windows app to close and wait for all owned processes to stop. +4. Save refreshed outgoing credentials to matching identity profiles. +5. Activate the selected credential using the existing rollback mechanism. +6. Copy the selected rollout into the desktop sessions directory and journal + its source hash. +7. Relaunch the Windows app. +8. Instruct the user to verify the account/workspace and open the transferred + thread from chronological history. + +Returning the thread: + +1. Stop the Windows app. +2. Compare source, checkout, and current desktop hashes. +3. Copy changes back only when the source has not changed. +4. Report a conflict instead of overwriting concurrent changes. +5. Validate the returned thread in its source history profile. +6. Keep or roll back the desktop credential according to the existing + confirmation flow. + +This is a checked-out thread with conflict detection, not shared concurrent +editing. The feature must be labeled experimental until the desktop app offers +a supported profile or thread handoff API. + +## Security and Reliability Requirements + +- Never return credentials or raw auth files to the webview. +- Require complete reusable OAuth bundles; reject access-token-only profiles. +- Use mode `0600` on Unix and restricted inherited ACLs on Windows. +- Preserve the existing refusal to activate through unsafe symlinks. +- Canonicalize all history paths and require them to remain within approved + roots. +- Treat chat transcripts as sensitive because prompts and tool output can + contain secrets. +- Never log chat bodies, auth payloads, or raw app-server event streams by + default. +- Stop writers before transferring a rollout. +- Treat externally discovered histories as read-only until explicit adoption. +- Detect likely external writers before every mutating operation; fail closed + when exclusive access cannot be established. +- Use deterministic lock ordering to prevent deadlocks. +- Journal every mutating operation before the first filesystem change. +- Make all transfers restartable or rollback-capable after process failure. +- Reject a destination collision when the same thread ID has different + content. +- Record the Codex version used for each runtime and generated protocol schema. +- Refuse unsupported protocol versions instead of falling back to SQLite. + +## Delivery Plan + +### Phase 1: Read-only history inventory + +- Add history profile discovery and canonical-root grouping. +- Add an app-server process adapter over stdio. +- Generate and check in version-matched protocol types. +- Implement `thread/list` and `thread/read`. +- Add the History view with no mutation controls. + +Exit criteria: openProfiler can display the same thread inventory and details +that Codex app-server reports for each discovered history profile without +reading SQLite directly. + +### Phase 2: Runtime binding and resume + +- Add single-writer leases and runtime lifecycle management. +- Activate a selected identity into a selected history runtime. +- Verify identity through `account/read`. +- Implement resume, fork, streamed events, and credential refresh write-back. + +Exit criteria: a user can independently select an identity and history, resume +a thread, stop the runtime, and retain both the updated transcript and refreshed +credential in their owning profiles. + +### Phase 3: Copy and move + +- Add transfer journals, hashing, atomic copy, and destination validation. +- Implement copy first. +- Implement move as validated copy plus source archive. +- Add collision, interruption, and recovery tests. + +Exit criteria: an interrupted operation cannot silently lose or overwrite a +thread, and every completed destination passes `thread/read`. + +### Phase 4: Windows desktop handoff + +- Extend the existing desktop rollback transaction with a thread checkout. +- Add source hash and conflict detection. +- Add return, keep, and rollback operations. + +Exit criteria: the Windows app can resume a checked-out thread under the +selected verified identity, and openProfiler can safely return or roll back the +handoff after the app is closed. + +### Phase 5: Experimental external authentication + +- Evaluate `chatgptAuthTokens` behind an opt-in feature. +- Integrate a secure token provider and refresh callback. +- Threat-model token rotation, revocation, timeout, and multi-account + concurrency. + +Exit criteria: external authentication is never required for the managed +profile path and can be removed without changing history storage. + +## Test Strategy + +Unit tests: + +- canonical root grouping and path escape rejection; +- identity/history binding validation; +- collision and duplicate-content handling; +- transfer state-machine transitions; +- deterministic lock ordering; +- credential and transcript redaction; +- desktop three-way hash comparison. + +Integration tests with a temporary `CODEX_HOME`: + +- generated client initializes against the supported Codex version; +- `thread/list` repairs metadata from copied JSONL; +- `thread/read`, resume, fork, archive, and unarchive; +- process interruption at every transfer state; +- app-server restart and stale-lock recovery; +- refreshed credential write-back without exposing token material. + +Windows tests: + +- packaged app process detection and graceful shutdown; +- ACL preservation; +- credential plus thread rollback; +- WSL profile source to Windows desktop destination; +- conflict when source and desktop copies both change. + +## Non-Goals + +- Editing Codex SQLite databases. +- Rewriting rollout JSONL events. +- Merging two divergent copies of one thread. +- Switching browser cookies or ChatGPT web sessions. +- Hot-switching the running Windows ChatGPT/Codex app. +- Treating Codex `--profile` as credential isolation. +- Making experimental external-token authentication the default. +- Providing Claude chat-history management through Codex app-server. + +## References + +- [Codex App Server](https://learn.chatgpt.com/docs/app-server) +- [Codex authentication](https://learn.chatgpt.com/docs/auth) +- [Codex configuration and state locations](https://learn.chatgpt.com/docs/config-file/config-advanced#config-and-state-locations) +- [Windows app: share config, auth, and sessions with WSL](https://learn.chatgpt.com/docs/windows/windows-app#share-config-auth-and-sessions-with-wsl) +- [Open-source Codex app-server](https://github.com/openai/codex/tree/main/codex-rs/app-server) diff --git a/docs/codex-token-management-architecture.md b/docs/codex-token-management-architecture.md new file mode 100644 index 0000000..13b7dd2 --- /dev/null +++ b/docs/codex-token-management-architecture.md @@ -0,0 +1,584 @@ +# Codex Credential Rotation and Synchronization Architecture + +Status: proposed +Target: openProfiler after `0.1.4` +Scope: one workstation with Windows and WSL Codex clients + +Related feature: [Cloud Credential Vault and Broker](cloud-credential-vault-feature.md) + +This document defines local Codex refresh ownership and runtime +synchronization. The cloud credential feature extends the same single-writer +model across workstations. Once implemented, its exact Key Vault secret version +becomes the durable accepted credential generation; local WSL profile files +become checked-out runtime copies rather than the remote authority. + +## Purpose + +openProfiler manages Codex identity profiles in WSL and can activate one of +those credentials in the Windows ChatGPT/Codex app. A ChatGPT OAuth credential +is not a static token: Codex can refresh it and replace its refresh token. + +Copying one `auth.json` into two independently running Codex homes creates two +writers. When either writer rotates the refresh token, the other copy can +become stale and later fail with: + +```text +Your access token could not be refreshed because your refresh token was revoked. +``` + +The solution is a credential broker with one canonical identity record and one +refresh owner at a time. It is not an unrestricted two-way file watcher. + +## Current Layout + +The canonical profile inventory is in WSL: + +```text +~/.config/workbenches/openai-profiles.json +~/.chatgpt-profiles/profiles////auth.json +``` + +For example: + +```text +~/.chatgpt-profiles/profiles/opensoft/max/max-001/auth.json +``` + +The Windows ChatGPT/Codex app has one active Codex home: + +```text +C:\Users\\.codex\auth.json +``` + +Windows does not have a separate profile inventory by default. The native +openProfiler app discovers WSL profiles and checks a selected credential into +the Windows active home. + +## Incident Model + +The `max-001` failure demonstrated the race: + +1. WSL had a valid `max-001` credential. +2. The credential was copied into the Windows Codex home. +3. Windows refreshed it and received a new refresh-token generation. +4. WSL retained the older generation. +5. `pcodex max001` attempted to refresh with the revoked WSL generation. + +The obsolete `opensoft-max-1` profile was unrelated. The failure came from two +copies of the same real account progressing independently. + +## Architecture Decisions + +### Keep one canonical credential authority + +Each `IdentityProfile` has one canonical complete credential bundle. With the +current profile layout, the authority is the canonical WSL profile: + +```text +~/.chatgpt-profiles/profiles//auth.json +``` + +Windows and provider runtime homes contain checked-out working copies. They are +not independent authorities. + +### Allow one refresh owner + +For each ChatGPT account ID, exactly one runtime may own refresh rights: + +```text +none | windows | wsl: +``` + +The owner may make authenticated requests, refresh credentials, and publish a +new generation. Other copies must be dormant. A client that is running outside +the broker is an unmanaged writer and prevents a safe ownership transfer. + +### Synchronize complete credential bundles + +openProfiler copies the complete validated `auth.json`, including: + +- `auth_mode`; +- access token; +- refresh token; +- ID token; +- ChatGPT account ID; +- refresh metadata. + +It must never copy only an access token or merge fields from different files. + +### Do not use modification time as authority + +Filesystem modification time, access-token expiry, and `last_refresh` are +evidence, not authority. A stale process can write an older credential after a +newer file was installed. + +An update is accepted when: + +1. it comes from the current lease owner; +2. its account identity matches the selected profile; +3. the source file is stable throughout the snapshot; +4. the complete bundle passes structural validation. + +If two non-identical credentials changed without an established owner, +openProfiler enters conflict state instead of selecting the newest timestamp. + +### Never overwrite a running non-owner + +Reading a stable snapshot from a running owner is allowed. Writing a credential +into a running target is not reliable because the process may cache its old +credential and later overwrite the new file. + +Stopping Windows is therefore not required for a read-only Windows-to-WSL +snapshot. It is required when WSL becomes the new refresh owner because the +Windows app has no documented pause-refresh operation. + +### Treat file watching as detection + +Watchers detect that the owner may have rotated its credential. They do not +decide ownership. + +Codex can replace `auth.json` atomically, so openProfiler watches the parent +directory, debounces events, reopens the file, and confirms that two successive +reads have the same hash before accepting the snapshot. + +## System Model + +```text +Canonical IdentityProfile + | + v + openProfiler broker + / \ + v v +Windows home WSL runtime home +``` + +The broker is the only component allowed to: + +- grant or release a refresh lease; +- activate a credential in a runtime home; +- accept a rotated generation; +- copy a generation to dormant mirrors; +- persist recovery metadata; +- declare and resolve conflicts. + +Codex itself continues to own the managed ChatGPT OAuth refresh operation. +openProfiler coordinates where that operation is allowed to occur. + +## Domain Model + +### CredentialAuthority + +Non-secret metadata describing the canonical identity: + +```text +provider +identity_profile_id +expected_email +expected_account_id +authority_path +credential_store +accepted_generation +accepted_last_refresh +accepted_credential_hash +accepted_refresh_hash +owner +lease_id +lease_started_at +state +``` + +Hashes are comparison fingerprints, not authentication material. They must +still be treated as private operational metadata and not exposed in normal UI +or logs. + +### CredentialCheckout + +One activated working copy: + +```text +target +path +identity_profile_id +account_id +generation_hash +process_id +process_started_at +last_observed_at +status +``` + +Targets include `windows` and a WSL runtime ID. + +### CredentialLease + +The single-writer grant: + +```text +lease_id +account_id +owner +checkout_id +process_id +issued_at +heartbeat_at +expires_at +state +``` + +Lease states are `requested`, `active`, `releasing`, `released`, `stale`, and +`conflict`. + +### CredentialEvent + +A sanitized audit entry: + +```text +event_id +account_id +identity_profile_id +event_type +source +previous_generation +new_generation +occurred_at +result +error_code +``` + +Events never contain token values, serialized credentials, command output, or +raw JWT claims. + +## State Machine + +```text + +-------------+ + | idle | + +------+------+ + | + checkout(target) + | + v + +---------+---------+ + | owner: | + +----+----------+---+ + | | + rotation release + | | + v v + publishing reconciling + | | + +----+-----+ + | + v + idle + +Any unowned divergence -> conflict +Interrupted operation -> recovery_required +``` + +Only an active owner can transition through `rotation` and `publishing`. + +## Credential Validation + +Before accepting or activating a credential, the backend verifies: + +1. the source and destination are regular files, not symlinks; +2. `auth_mode` is `chatgpt`; +3. the bundle contains access, refresh, and ID tokens; +4. the ID token email matches the profile's declared email; +5. the ChatGPT account ID matches the recorded account ID; +6. the source hash remains stable across the copy; +7. the destination hash equals the accepted source hash; +8. Unix mode is `0600`, or Windows ACLs remain restricted; +9. no credential value is returned to the webview or logs. + +An access token that currently works is not sufficient proof that the refresh +token remains valid. After ownership transfer, openProfiler should use +`account/read` without forcing refresh, followed by a minimal real request when +the user requests end-to-end verification. + +## Core Workflows + +### Start Windows as owner + +1. Resolve the selected canonical WSL profile. +2. Acquire the account lease for `windows`. +3. Confirm no WSL runtime owns or is using the account. +4. Validate the canonical credential. +5. Stop the Windows app if its active identity must be changed. +6. Preserve the existing credential rollback transaction. +7. Atomically install the canonical credential into the Windows Codex home. +8. Launch the Windows app. +9. Record its process and checkout generation. +10. Watch the Windows Codex home for credential replacement. + +If Windows was already running as the verified owner, a read-only snapshot does +not require stopping it. + +### Publish a Windows rotation + +1. Detect replacement of the Windows `auth.json`. +2. Confirm Windows holds the active lease. +3. Wait for the file to become stable. +4. Validate identity and credential structure. +5. Compare it with the accepted generation. +6. Atomically replace the canonical WSL credential. +7. Verify the canonical destination hash and permissions. +8. Update non-secret generation metadata. +9. Update only dormant mirrors. + +The running Windows credential remains the owner. WSL runtimes may not start +with this account until ownership transfers. + +### Transfer ownership from Windows to WSL + +1. Acquire a transfer transaction for the account. +2. Snapshot and validate the current Windows credential. +3. Stop the Windows app or otherwise prove it cannot make requests. +4. Re-read Windows after shutdown. +5. If the credential changed, accept the final stable owner generation. +6. Publish that generation to the canonical WSL profile. +7. Activate the canonical credential in the selected WSL history runtime. +8. Start Codex and verify the expected account. +9. Grant the lease to the WSL runtime. + +The stop occurs for ownership transfer, not because reading the Windows +credential inherently requires shutdown. + +### Run `pcodex` + +The profile launcher should use the broker: + +```text +pcodex max001 + -> resolve canonical profile + -> acquire WSL lease + -> reconcile previous owner + -> launch Codex + -> observe credential changes + -> check in final credential + -> release lease +``` + +If Windows currently owns the account, the launcher blocks with an actionable +message rather than starting with a duplicated refresh token. + +### Publish a WSL rotation + +1. Detect replacement of the owning WSL runtime's `auth.json`. +2. Validate identity, structure, ownership, and file stability. +3. Publish the accepted generation to the canonical identity profile. +4. Copy it to Windows only when the Windows app is stopped. +5. Otherwise mark the Windows checkout stale and update it at its next + activation. + +### Release WSL ownership + +1. Wait for Codex to become idle and exit. +2. Read the final WSL credential. +3. Publish any accepted rotation to the authority. +4. Update dormant mirrors. +5. Remove temporary runtime credentials where applicable. +6. Release the lease. + +### Snapshot without ownership transfer + +openProfiler may copy the current owner credential to the canonical authority +while the owner remains running when: + +- the source is stable across the snapshot; +- the owner lease is valid; +- no other runtime is permitted to use the copied refresh token; +- the copy is treated as a dormant recovery mirror. + +This is how Windows rotations can continuously update WSL without stopping +Windows. It does not authorize concurrent `pcodex` use. + +## Conflict Handling + +A conflict exists when: + +- Windows and WSL both changed while neither had a valid lease; +- a non-owner writes a different refresh generation; +- account identity differs from the selected profile; +- the source changes during snapshot; +- the broker restarts with multiple apparently active writers; +- a stale process writes after an ownership transfer. + +Resolution rules: + +1. Stop or quiesce all writers. +2. Never select by modification time alone. +3. Prefer the final credential from the recorded lease owner. +4. Verify identity before installing it anywhere. +5. Preserve restricted rollback copies until validation succeeds. +6. If ownership cannot be established, require a fresh managed ChatGPT login. + +The broker must never repeatedly test competing refresh tokens. Refreshing one +candidate can invalidate another and make diagnosis less reliable. + +## Process and IPC Architecture + +The native openProfiler process should own the broker state because it already +controls the packaged Windows app and can access registered WSL profile stores. + +The WSL `pcodex` wrapper needs a narrow authenticated control interface: + +```text +credential/status +credential/lease/acquire +credential/lease/heartbeat +credential/publish +credential/lease/release +``` + +The transport can be an authenticated loopback endpoint or another +cross-Windows/WSL local IPC mechanism. Requirements: + +- loopback only; +- high-entropy capability credential stored with restricted Windows ACLs; +- no raw OAuth credential in RPC messages; +- broker reads credentials directly from approved paths; +- request IDs and idempotency for mutating operations; +- bounded timeouts and stale-lease recovery; +- no generic filesystem or command-execution endpoint. + +Until the broker is available, launchers should warn when the same account is +active in Windows and WSL. openProfiler cannot guarantee safety for clients +started outside its lifecycle. + +## Atomic File Operations + +Credential publication uses: + +1. an approved source path; +2. a temporary file in the destination directory; +3. restrictive permissions before writing; +4. full write and file flush; +5. source stability recheck; +6. atomic rename over the destination; +7. destination-directory flush where supported; +8. post-write hash and identity verification. + +Windows activation preserves the destination ACL. Unix profile credentials use +mode `0600`. Symlinked credential files and active homes are rejected. + +## SOPS Escrow + +SOPS is a recovery snapshot, not the live synchronization channel or lease +authority. + +An escrow policy must account for refresh-token rotation: + +- never restore escrow over a newer accepted live generation; +- update escrow only after a generation is accepted by the broker; +- debounce automatic escrow updates to avoid unnecessary repository churn; +- verify decrypt-and-hash round trips without printing plaintext; +- record only non-secret account and generation metadata; +- treat an old escrow refresh token as potentially revoked; +- require login when a restored credential cannot refresh. + +For reliable cross-workstation concurrent use, a workstation-local broker is +insufficient. That requires a remote credential authority and distributed +lease. Copying one refresh token from SOPS or Key Vault to multiple active +workstations recreates the same race. + +## Security Requirements + +- Credentials never enter React state or Tauri event payloads. +- Token values and raw JWTs never appear in logs. +- Account email and account ID are used only for identity verification. +- Credential fingerprints are hidden from normal UI. +- Rollback credentials remain mode `0600` or under restricted Windows ACLs. +- Watch the parent directory to detect atomic file replacement. +- Do not follow symlinks when resolving credential sources or destinations. +- Refuse account mismatches even when the access token currently succeeds. +- Do not copy into a running non-owner process. +- Fail closed when lease ownership is uncertain. +- Keep a bounded sanitized audit trail for recovery and support. + +## Delivery Plan + +### Phase 1: Credential inventory and diagnostics + +- Add non-secret credential metadata and duplicate-account detection. +- Show canonical authority, active checkout, owner, and stale mirrors. +- Detect Windows/WSL divergence without modifying credentials. + +### Phase 2: Atomic snapshot and publication + +- Implement stable snapshot, validation, atomic write, and rollback. +- Publish rotations from a known owner to the canonical profile. +- Add explicit Windows-to-WSL and WSL-to-Windows reconciliation commands. + +### Phase 3: Single-writer leases + +- Add per-account lease state and process heartbeats. +- Integrate Windows lifecycle management. +- Add WSL broker IPC and `pcodex` wrapper integration. +- Block concurrent refresh writers. + +### Phase 4: Automatic rotation observation + +- Watch owner credential directories. +- Publish accepted rotations to the authority. +- Mark running or unavailable mirrors stale instead of overwriting them. +- Add crash recovery and conflict UI. + +### Phase 5: Escrow policy + +- Trigger encrypted backup after accepted rotation according to policy. +- Validate registry round trips. +- Add optional remote authority design for multiple workstations. + +## Test Strategy + +Unit tests: + +- complete bundle validation; +- account and email mismatch rejection; +- source stability checks; +- generation comparison; +- owner-only publication; +- lease expiry and recovery; +- conflict transitions; +- path and symlink rejection; +- sanitized event serialization. + +Integration tests: + +- Windows rotation publishes to dormant WSL authority; +- WSL rotation updates Windows only while Windows is stopped; +- copying into a running non-owner is refused; +- stale process write after transfer enters conflict; +- interrupted atomic publication preserves one complete credential; +- owner process exits during refresh; +- broker restart with a stale lease; +- real Codex status and inference after controlled transfer. + +Regression scenario: + +1. seed Windows and WSL with one generation; +2. rotate Windows to a second generation; +3. confirm WSL is marked stale; +4. publish Windows to canonical WSL; +5. transfer ownership to WSL; +6. confirm the old WSL generation can never overwrite the accepted generation. + +## Non-Goals + +- Concurrent independent refresh writers for one ChatGPT account. +- Selecting credential authority by modification time. +- Sharing only an access token. +- Editing credentials in a running non-owner. +- Treating SOPS or Key Vault as an OAuth refresh broker. +- Managing browser cookies or ChatGPT web sessions. +- Using experimental externally managed ChatGPT tokens in the first release. + +## References + +- [Codex authentication](https://learn.chatgpt.com/docs/auth) +- [Codex App Server](https://learn.chatgpt.com/docs/app-server) +- [Codex environment variables](https://learn.chatgpt.com/docs/config-file/environment-variables) +- [Windows app: share config, auth, and sessions with WSL](https://learn.chatgpt.com/docs/windows/windows-app#share-config-auth-and-sessions-with-wsl)