From b8225b4c14e2ad5967645fbb1d003377d2842ea8 Mon Sep 17 00:00:00 2001
From: Brett Heap <1513478+brettheap@users.noreply.github.com>
Date: Wed, 9 Sep 2026 17:09:10 +0000
Subject: [PATCH] docs: preserve profile architecture proposals
---
README.md | 7 +
docs/claude-oauth-enrollment-automation.md | 644 +++++++++++++++++++
docs/cloud-credential-vault-feature.md | 574 +++++++++++++++++
docs/codex-history-profile-architecture.md | 653 ++++++++++++++++++++
docs/codex-token-management-architecture.md | 584 +++++++++++++++++
5 files changed, 2462 insertions(+)
create mode 100644 docs/claude-oauth-enrollment-automation.md
create mode 100644 docs/cloud-credential-vault-feature.md
create mode 100644 docs/codex-history-profile-architecture.md
create mode 100644 docs/codex-token-management-architecture.md
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)