Commit Graph

45 Commits

  • core tests: submit turns with permission profiles (#20010)
    ## Summary
    
    - Add `PermissionProfile`-based turn submission helpers to
    `core_test_support`, while keeping the legacy `SandboxPolicy` helper for
    tests that intentionally exercise legacy fallback behavior.
    - Switch the default `TestCodex::submit_turn()` path to send a real
    `PermissionProfile` plus the required legacy compatibility projection in
    `Op::UserTurn`.
    - Migrate straightforward app/search/shell/truncation tests from
    `SandboxPolicy::{DangerFullAccess, ReadOnly}` to
    `PermissionProfile::{Disabled, read_only}`.
    - Add a TUI compatibility projection helper for legacy app-server fields
    so non-legacy writable roots are preserved instead of being downgraded
    to read-only.
    - Fix remote start/resume/fork sandbox-mode projection to classify any
    managed profile with writable roots as workspace-write, not only
    profiles that can write `cwd`.
    - Reduce `SandboxPolicy` references in `codex-rs/core/tests` from 47
    files to 41 files without changing production behavior.
    
    ## Testing
    
    - `cargo check -p codex-core --tests`
    - `cargo test -p codex-tui
    compatibility_profile_preserves_unbridgeable_write_roots`
    - `cargo test -p codex-tui
    sandbox_mode_preserves_non_cwd_write_roots_for_remote_sessions`
    - `just fmt`
    - `just fix -p core_test_support`
    - `just fix -p codex-core`
  • tui: use permission profiles for sandbox state (#20008)
    ## Summary
    - Move TUI permission state from legacy `SandboxPolicy` values to
    canonical `PermissionProfile` values across presets, app events, chat
    widget state, app commands, thread routing, and cached thread session
    state.
    - Keep app-server compatibility boundaries explicit: embedded sessions
    send `permissionProfile`, while remote sessions send only a legacy
    `sandbox` projection and fall back to read-only when a custom profile
    cannot be projected.
    - Update status/add-dir UI summaries and snapshots to render the active
    permission profile, including workspace profiles selected by the new
    built-in defaults.
    
    ## Verification
    - `rg '\bSandboxPolicy\b' codex-rs/tui -n` returns no matches.
    - `cargo test -p codex-tui`
    - `cargo check -p codex-tui --tests`
    - `cargo test -p codex-tui additional_dirs`
    - `just fmt`
    - `just fix -p codex-tui`
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/20008).
    * #20041
    * #20040
    * #20037
    * #20035
    * #20034
    * #20033
    * #20032
    * #20030
    * #20028
    * #20027
    * #20026
    * #20024
    * #20021
    * #20018
    * #20016
    * #20015
    * #20013
    * #20011
    * #20010
    * __->__ #20008
  • permissions: store thread sessions as profiles (#19776)
    ## Why
    
    After thread sessions have a required `PermissionProfile`, the TUI no
    longer needs to cache a separate legacy `SandboxPolicy` in
    `ThreadSessionState`. Keeping the legacy field would reintroduce two
    permission authorities in the session cache and make later
    replay/switching logic easier to get wrong.
    
    This PR keeps legacy app-server compatibility at the ingestion boundary:
    old `sandbox` response values are still accepted, but they are
    immediately converted to a cwd-anchored profile.
    
    ## What Changed
    
    - Removes `ThreadSessionState.sandbox_policy`.
    - Updates active-session permission syncing to write only the current
    `PermissionProfile`.
    - Updates thread-read/replay/test fixtures to use profiles as the cached
    session permission source.
    - Leaves legacy `sandbox` fields in app-server request/response protocol
    paths unchanged; those are compatibility boundaries and are converted
    before entering cached TUI state.
    
    ## Verification
    
    - `cargo test -p codex-tui thread_session_state::tests --lib`
    - `cargo test -p codex-tui
    inactive_thread_started_notification_initializes_replay_session --lib`
    - `cargo test -p codex-tui thread_events --lib`
    - `just fix -p codex-tui`
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19776).
    * #19900
    * #19899
    * __->__ #19776
  • permissions: require profiles in TUI thread state (#19773)
    ## Why
    
    `ThreadSessionState` is the TUI's cached view of an app-server session.
    To make `PermissionProfile` the canonical runtime permissions model,
    cached thread sessions need to always have a profile instead of treating
    the profile as an optional supplement to a legacy `sandbox` response
    field.
    
    The main compatibility concern is older app-server v2 lifecycle
    responses that only include `sandbox` and omit `permissionProfile`:
    
    - `thread/start` -> `ThreadStartResponse.sandbox`
    - `thread/resume` -> `ThreadResumeResponse.sandbox`
    - `thread/fork` -> `ThreadForkResponse.sandbox`
    
    Those responses must still hydrate correctly when the TUI is pointed at
    an older app-server. This PR converts the legacy `sandbox` value into a
    `PermissionProfile` immediately at response ingestion time, using the
    response `cwd`, so cached sessions do not carry an optional profile that
    can later reinterpret cwd-bound grants against a different thread cwd.
    
    This fallback is intentionally boundary compatibility. The follow-up PRs
    in this stack continue the cleanup by making `SessionConfiguredEvent`
    profile-only, deriving sandbox projections from snapshots only when an
    API still needs them, and then removing `sandbox_policy` from
    `ThreadSessionState`.
    
    ## What Changed
    
    - Makes `ThreadSessionState.permission_profile` required.
    - Converts legacy app-server response `sandbox` values into a
    `PermissionProfile` at ingestion time using the response cwd.
    - Ensures `thread/read` hydration does not reuse a primary session
    profile that may be anchored to a different cwd; it uses the active
    widget permission settings for the read thread fallback instead of
    reusing cached primary-session permissions.
    - Keeps the app-server request path unchanged: embedded sessions send
    profiles, while remote sessions continue using legacy sandbox overrides
    for compatibility.
    
    ## Verification
    
    - `cargo test -p codex-tui thread_read --lib`
    - `cargo test -p codex-tui
    permission_settings_sync_preserves_active_profile_only_rules --lib`
    - `cargo test -p codex-tui
    resume_response_restores_turns_from_thread_items --lib`
    - `cargo test -p codex-tui thread_session_state::tests --lib`
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19773).
    * #19900
    * #19899
    * #19776
    * #19775
    * #19774
    * __->__ #19773
  • Add /auto-review-denials retry approval flow (#19058)
    ## Why
    
    Auto-review can deny an action that the user later decides they want to
    retry. Today there is no TUI surface for selecting a recent denial and
    sending explicit approval context back into the session, so users have
    to restate intent manually and the retry can be reviewed without the
    original denied action context.
    
    This adds a narrow TUI-driven path for approving a recent denied action
    while still keeping the retry inside the normal auto-review flow.
    
    ## What Changed
    
    - Added `/auto-review-denials` to open a picker of recent denied
    auto-review actions.
    - Added a small in-memory TUI store for the 10 most recent denied
    auto-review events.
    - Selecting a denial sends the structured denied event back through the
    existing core/app-server op path.
    - Core now injects a developer message containing the approved action
    JSON rather than the full assessment event.
    - Auto-review transcript collection now preserves this specific approval
    developer message so follow-up review sessions can see the user approval
    context.
    - Added TUI snapshot/unit coverage for the picker and approval dispatch
    path.
    - Added core coverage for retaining the approval developer message in
    the auto-review transcript.
    
    ## Verification
    
    - `cargo test -p codex-core
    collect_guardian_transcript_entries_keeps_manual_approval_developer_message`
    - `cargo test -p codex-tui auto_review_denials`
    - `cargo test -p codex-tui
    approving_recent_denial_emits_structured_core_op_once`
    
    ## Notes
    
    This intentionally keeps retries going through auto-review. The approval
    signal is context for the exact previously denied action, not a blanket
    bypass for similar future actions.
  • permissions: centralize legacy sandbox projection (#19734)
    ## Why
    
    The remaining migration work still needs `SandboxPolicy` at a few
    compatibility boundaries, but those projections should come from one
    canonical path. Keeping ad hoc legacy projections scattered through
    app-server, CLI, and config code makes it easy for behavior to drift as
    `PermissionProfile` gains fidelity that the legacy enum cannot
    represent.
    
    ## What Changed
    
    - Adds `Permissions::legacy_sandbox_policy(cwd)` and
    `Config::legacy_sandbox_policy()` as the compatibility projection from
    the canonical `PermissionProfile`.
    - Adds `Permissions::can_set_legacy_sandbox_policy()` so legacy inputs
    are checked after they are converted into profile semantics.
    - Updates app-server command handling, Windows sandbox setup, session
    configuration, and sandbox summaries to use the centralized projection
    helper.
    - Leaves `SandboxPolicy` in place only for boundary inputs/outputs that
    still speak the legacy abstraction.
    
    ## Verification
    
    - `cargo check -p codex-config -p codex-core -p codex-sandboxing -p
    codex-app-server -p codex-cli -p codex-tui`
    - `cargo test -p codex-tui
    permissions_selection_history_snapshot_full_access_to_default --
    --nocapture`
    - `cargo test -p codex-tui
    permissions_selection_sends_approvals_reviewer_in_override_turn_context
    -- --nocapture`
    - `bazel test //codex-rs/tui:tui-unit-tests-bin
    --test_arg=permissions_selection_history_snapshot_full_access_to_default
    --test_output=errors`
    - `bazel test //codex-rs/tui:tui-unit-tests-bin
    --test_arg=permissions_selection_sends_approvals_reviewer_in_override_turn_context
    --test_output=errors`
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19734).
    * #19737
    * #19736
    * #19735
    * __->__ #19734
  • permissions: make runtime config profile-backed (#19606)
    ## Why
    
    This supersedes #19391. During stack repair, GitHub marked #19391 as
    merged into a temporary stack branch rather than into `main`, so the
    runtime-config change needed a fresh PR.
    
    `PermissionProfile` is now the canonical permissions shape after #19231
    because it can distinguish `Managed`, `Disabled`, and `External`
    enforcement while also carrying filesystem rules that legacy
    `SandboxPolicy` cannot represent cleanly. Core config and session state
    still needed to accept profile-backed permissions without forcing every
    profile through the strict legacy bridge, which rejected valid runtime
    profiles such as direct write roots.
    
    The unrelated CI/test hardening that previously rode along with this PR
    has been split into #19683 so this PR stays focused on the permissions
    model migration.
    
    ## What Changed
    
    - Adds `Permissions.permission_profile` and
    `SessionConfiguration.permission_profile` as constrained runtime state,
    while keeping `sandbox_policy` as a legacy compatibility projection.
    - Introduces profile setters that keep `PermissionProfile`, split
    filesystem/network policies, and legacy `SandboxPolicy` projections
    synchronized.
    - Uses a compatibility projection for requirement checks and legacy
    consumers instead of rejecting profiles that cannot round-trip through
    `SandboxPolicy` exactly.
    - Updates config loading, config overrides, session updates, turn
    context plumbing, prompt permission text, sandbox tags, and exec request
    construction to carry profile-backed runtime permissions.
    - Preserves configured deny-read entries and `glob_scan_max_depth` when
    command/session profiles are narrowed.
    - Adds `PermissionProfile::read_only()` and
    `PermissionProfile::workspace_write()` presets that match legacy
    defaults.
    
    ## Verification
    
    - `cargo test -p codex-core direct_write_roots`
    - `cargo test -p codex-core runtime_roots_to_legacy_projection`
    - `cargo test -p codex-app-server
    requested_permissions_trust_project_uses_permission_profile_intent`
    
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19606).
    * #19395
    * #19394
    * #19393
    * #19392
    * __->__ #19606
  • Add goal TUI UX (5 / 5) (#18077)
    Adds the TUI user experience for goals on top of the core runtime from
    PR 4.
    
    ## Why
    
    Users need a direct TUI control surface for long-running goals. The UI
    should make the current goal visible, support common goal actions
    without waiting for a model turn, and avoid confusing end-of-turn
    notifications while an active goal is immediately continuing.
    
    ## What changed
    
    - Added `/goal` summary rendering for the current goal, including
    active, paused, budget-limited, and complete states.
    - Added `/goal <objective>` creation/replacement through the app-server
    goal API rather than a model prompt.
    - Added `/goal clear`, `/goal pause`, and `/goal unpause` command
    variants.
    - Added a confirmation menu when the user enters a new goal while
    another goal already exists.
    - Updated `/goal` help and summary tip text so it reflects the supported
    command variants without advertising slash-command token budgets.
    - Added footer/statusline goal indicators, including elapsed time and
    token budget display when a budget exists from API/tool-created goals.
    - Consumes goal updated/cleared notifications so the TUI stays in sync
    with external app-server changes.
    - Suppresses end-of-turn desktop notifications only when a goal is still
    active and follow-up work is expected.
    - Preserves slash-command history behavior and avoids leaking queued
    `/goal` state into unrelated submissions.
    
    ## Verification
    
    - Added TUI unit and snapshot coverage for goal command availability,
    summary rendering, control commands, replacement menu behavior,
    status/footer display, notification handling, and command history.
  • permissions: make legacy profile conversion cwd-free (#19414)
    ## Why
    
    The profile conversion path still required a `cwd` even when it was only
    translating a legacy `SandboxPolicy` into a `PermissionProfile`. That
    made profile producers invent an ambient `cwd`, which is exactly the
    anchoring we are trying to remove from permission-profile data. A legacy
    workspace-write policy can be represented symbolically instead: `:cwd =
    write` plus read-only `:project_roots` metadata subpaths.
    
    This PR creates that cwd-free base so the rest of the stack can stop
    threading cwd through profile construction. Callers that actually need a
    concrete runtime filesystem policy for a specific cwd still have an
    explicitly named cwd-bound conversion.
    
    ## What Changed
    
    - `PermissionProfile::from_legacy_sandbox_policy` now takes only
    `&SandboxPolicy`.
    - `FileSystemSandboxPolicy::from_legacy_sandbox_policy` is now the
    symbolic, cwd-free projection for profiles.
    - The old concrete projection is retained as
    `FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd` for
    runtime/boundary code that must materialize legacy cwd behavior.
    - Workspace-write profiles preserve `CurrentWorkingDirectory` and
    `ProjectRoots` special entries instead of materializing cwd into
    absolute paths.
    
    ## Verification
    
    - `cargo check -p codex-protocol -p codex-core -p
    codex-app-server-protocol -p codex-app-server -p codex-exec -p
    codex-exec-server -p codex-tui -p codex-sandboxing -p
    codex-linux-sandbox -p codex-analytics --tests`
    - `just fix -p codex-protocol -p codex-core -p codex-app-server-protocol
    -p codex-app-server -p codex-exec -p codex-exec-server -p codex-tui -p
    codex-sandboxing -p codex-linux-sandbox -p codex-analytics`
    
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19414).
    * #19395
    * #19394
    * #19393
    * #19392
    * #19391
    * __->__ #19414
  • permissions: make profiles represent enforcement (#19231)
    ## Why
    
    `PermissionProfile` is becoming the canonical permissions abstraction,
    but the old shape only carried optional filesystem and network fields.
    It could describe allowed access, but not who is responsible for
    enforcing it. That made `DangerFullAccess` and `ExternalSandbox` lossy
    when profiles were exported, cached, or round-tripped through app-server
    APIs.
    
    The important model change is that active permissions are now a disjoint
    union over the enforcement mode. Conceptually:
    
    ```rust
    pub enum PermissionProfile {
        Managed {
            file_system: FileSystemSandboxPolicy,
            network: NetworkSandboxPolicy,
        },
        Disabled,
        External {
            network: NetworkSandboxPolicy,
        },
    }
    ```
    
    This distinction matters because `Disabled` means Codex should apply no
    outer sandbox at all, while `External` means filesystem isolation is
    owned by an outside caller. Those are not equivalent to a broad managed
    sandbox. For example, macOS cannot nest Seatbelt inside Seatbelt, so an
    inner sandbox may require the outer Codex layer to use no sandbox rather
    than a permissive one.
    
    ## How Existing Modeling Maps
    
    Legacy `SandboxPolicy` remains a boundary projection, but it now maps
    into the higher-fidelity profile model:
    
    - `ReadOnly` and `WorkspaceWrite` map to `PermissionProfile::Managed`
    with restricted filesystem entries plus the corresponding network
    policy.
    - `DangerFullAccess` maps to `PermissionProfile::Disabled`, preserving
    the “no outer sandbox” intent instead of treating it as a lax managed
    sandbox.
    - `ExternalSandbox { network_access }` maps to
    `PermissionProfile::External { network }`, preserving external
    filesystem enforcement while still carrying the active network policy.
    - Split runtime policies that legacy `SandboxPolicy` cannot faithfully
    express, such as managed unrestricted filesystem plus restricted
    network, stay `Managed` instead of being collapsed into
    `ExternalSandbox`.
    - Per-command/session/turn grants remain partial overlays via
    `AdditionalPermissionProfile`; full `PermissionProfile` is reserved for
    complete active runtime permissions.
    
    ## What Changed
    
    - Change active `PermissionProfile` into a tagged union: `managed`,
    `disabled`, and `external`.
    - Keep partial permission grants separate with
    `AdditionalPermissionProfile` for command/session/turn overlays.
    - Represent managed filesystem permissions as either `restricted`
    entries or `unrestricted`; `glob_scan_max_depth` is non-zero when
    present.
    - Preserve old rollout compatibility by accepting the pre-tagged `{
    network, file_system }` profile shape during deserialization.
    - Preserve fidelity for important edge cases: `DangerFullAccess`
    round-trips as `disabled`, `ExternalSandbox` round-trips as `external`,
    and managed unrestricted filesystem + restricted network stays managed
    instead of being mistaken for external enforcement.
    - Preserve configured deny-read entries and bounded glob scan depth when
    full profiles are projected back into runtime policies, including
    unrestricted replacements that now become `:root = write` plus deny
    entries.
    - Regenerate the experimental app-server v2 JSON/TypeScript schema and
    update the `command/exec` README example for the tagged
    `permissionProfile` shape.
    
    ## Compatibility
    
    Legacy `SandboxPolicy` remains available at config/API boundaries as the
    compatibility projection. Existing rollout lines with the old
    `PermissionProfile` shape continue to load. The app-server
    `permissionProfile` field is experimental, so its v2 wire shape is
    intentionally updated to match the higher-fidelity model.
    
    ## Verification
    
    - `just write-app-server-schema`
    - `cargo check --tests`
    - `cargo test -p codex-protocol permission_profile`
    - `cargo test -p codex-protocol
    preserving_deny_entries_keeps_unrestricted_policy_enforceable`
    - `cargo test -p codex-app-server-protocol
    permission_profile_file_system_permissions`
    - `cargo test -p codex-app-server-protocol serialize_client_response`
    - `cargo test -p codex-core
    session_configured_reports_permission_profile_for_external_sandbox`
    - `just fix`
    - `just fix -p codex-protocol`
    - `just fix -p codex-app-server-protocol`
    - `just fix -p codex-core`
    - `just fix -p codex-app-server`
  • feat: expose AWS account state from account/read (#19048)
    ## Why
    
    AWS/Bedrock mode currently reports `account: null` with
    `requiresOpenaiAuth: false` from `account/read`. That suppresses the
    OpenAI-auth requirement, but it does not let app clients distinguish AWS
    auth from any other non-OpenAI custom provider. For the prototype AWS
    provider UX, clients need a simple provider-derived signal so they can
    suppress ChatGPT/API-key login and token-refresh paths without
    hardcoding Bedrock checks.
    
    ## What changed
    
    - Adds an `aws` variant to the v2 `Account` protocol union.
    - Adds `ProviderAccountKind` to `codex-model-provider` so the runtime
    provider owns the app-visible account classification.
    - Makes Amazon Bedrock return `ProviderAccountKind::Aws` from the
    model-provider layer.
    - Updates app-server `account/read` to map `ProviderAccountKind` to the
    existing `GetAccountResponse` wire shape.
    - Preserves the existing `account: null, requiresOpenaiAuth: false`
    behavior for other non-OpenAI providers.
    - Regenerates the app-server protocol schema fixtures.
    - Adds coverage for provider account classification and for the Amazon
    Bedrock `account/read` response.
    
    ## Testing
    
    - `cargo test -p codex-model-provider`
    - `cargo test -p codex-app-server-protocol`
    - `cargo test -p codex-app-server get_account_with_aws_provider`
    
    ## Notes
    
    I attempted `just bazel-lock-update` and `just bazel-lock-check`, but
    both are blocked in my local environment because `bazel` is not
    installed.
  • tui: sync session permission profiles (#18284)
    ## Why
    
    Once `SessionConfigured` carries the active `PermissionProfile`, the TUI
    must treat that as authoritative session state. Otherwise the widget can
    keep stale local permission details after a session is configured or
    resumed.
    
    The TUI also keeps a local `Config` copy used for later operations, so
    session-sourced profiles and subsequent local sandbox changes need to
    keep the derived split runtime permissions in sync. Because this PR may
    land before the follow-up user-turn profile plumbing, embedded
    app-server turns also need a standalone path for carrying local runtime
    sandbox overrides.
    
    ## What changed
    
    - Sync the chat widget runtime filesystem/network permissions from
    `SessionConfigured.permission_profile`, with the legacy `sandbox_policy`
    as the fallback.
    - Recompute split runtime permissions whenever the TUI applies or
    carries forward a local sandbox-policy override.
    - Mark feature-driven Auto-review sandbox changes as runtime sandbox
    overrides so the standalone embedded turn-start profile path is used
    even without the follow-up user-turn profile PR.
    - Send a turn-start `permissionProfile` for embedded,
    non-ExternalSandbox turns when the TUI has a runtime sandbox override;
    remote and ExternalSandbox turns keep using the legacy sandbox field.
    - Extend coverage for profile sync, local sandbox changes,
    ExternalSandbox fallback, feature-driven sandbox overrides, and
    turn-start permission override selection.
    
    ## Verification
    
    - `cargo test -p codex-tui
    update_feature_flags_enabling_guardian_selects_auto_review`
    - `cargo test -p codex-tui
    turn_start_permission_overrides_send_profiles_only_for_embedded_runtime_overrides`
    - `cargo test -p codex-tui permission_settings_sync`
    - `cargo test -p codex-tui
    session_configured_external_sandbox_keeps_external_runtime_policy`
    - `cargo test -p codex-tui
    session_configured_syncs_widget_config_permissions_and_cwd`
    - `just fix -p codex-tui`
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/18284).
    * #18288
    * #18287
    * #18286
    * #18285
    * __->__ #18284
  • clients: send permission profiles to app-server (#18280)
    ## Why
    
    After app-server can accept `PermissionProfile`, first-party clients
    should stop preferring legacy sandbox fields when canonical permission
    information is available. This keeps the migration moving without
    removing legacy compatibility yet.
    
    The client side still has mixed surfaces during the stack: embedded
    thread start/resume/fork and exec initial turns can derive a profile
    directly from local config, while TUI remote sessions and some
    turn-start paths only have a legacy/server-context-safe sandbox
    projection. Those paths keep sending legacy sandbox fields rather than
    synthesizing or sending lossy/local-only profiles.
    
    ## What changed
    
    - Sends `permissionProfile` from exec and embedded TUI thread
    start/resume/fork requests when config has a representable profile.
    - Keeps legacy sandbox fallback for external sandbox policies, TUI
    remote thread lifecycle requests, and TUI turn-start requests that do
    not yet carry the active profile.
    - Sends the actual config-derived `permissionProfile` for exec initial
    turns instead of rebuilding one from the legacy sandbox projection.
    - Stores response `permissionProfile` as optional in TUI session state
    so external sandbox responses and compatibility payloads preserve
    `null`.
    - Updates tests for request construction and response mapping.
    
    ## Verification
    
    - `cargo check --tests -p codex-tui -p codex-exec`
    - `cargo test -p codex-tui app_server_session -- --nocapture`
    - `cargo test -p codex-exec thread_start_params -- --nocapture`
    - `cargo test -p codex-tui
    app_server_session::tests::thread_lifecycle_params -- --nocapture`
    - `just fix -p codex-tui -p codex-exec`
    - `just fix -p codex-tui`
    
    
    
    
    
    
    
    
    
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/18280).
    * #18288
    * #18287
    * #18286
    * #18285
    * #18284
    * #18283
    * #18282
    * #18281
    * __->__ #18280
  • app-server: accept permission profile overrides (#18279)
    ## Why
    
    `PermissionProfile` is becoming the canonical permissions shape shared
    by core and app-server. After app-server responses expose the active
    profile, clients need to be able to send that same shape back when
    starting, resuming, forking, or overriding a turn instead of translating
    through the legacy `sandbox`/`sandboxPolicy` shorthands.
    
    This still needs to preserve the existing requirements/platform
    enforcement model. A profile-shaped request can be downgraded or
    rejected by constraints, but the server should keep the user's
    elevated-access intent for project trust decisions. Turn-level profile
    overrides also need to retain existing read protections, including
    deny-read entries and bounded glob-scan metadata, so a permission
    override cannot accidentally drop configured protections such as
    `**/*.env = deny`.
    
    ## What changed
    
    - Adds optional `permissionProfile` request fields to `thread/start`,
    `thread/resume`, `thread/fork`, and `turn/start`.
    - Rejects ambiguous requests that specify both `permissionProfile` and
    the legacy `sandbox`/`sandboxPolicy` fields, including running-thread
    resume requests.
    - Converts profile-shaped overrides into core runtime filesystem/network
    permissions while continuing to derive the constrained legacy sandbox
    projection used by existing execution paths.
    - Preserves project-trust intent for profile overrides that are
    equivalent to workspace-write or full-access sandbox requests.
    - Preserves existing deny-read entries and `globScanMaxDepth` when
    applying turn-level `permissionProfile` overrides.
    - Updates app-server docs plus generated JSON/TypeScript schema fixtures
    and regression coverage.
    
    ## Verification
    
    - `cargo test -p codex-app-server-protocol schema_fixtures`
    - `cargo test -p codex-core
    session_configuration_apply_permission_profile_preserves_existing_deny_read_entries`
    
    
    
    
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/18279).
    * #18288
    * #18287
    * #18286
    * #18285
    * #18284
    * #18283
    * #18282
    * #18281
    * #18280
    * __->__ #18279
  • Add plumbing to approve stored Auto-Review denials (#18955)
    ## Summary
    
    This adds the structural plumbing needed for an app-server client to
    approve a previously denied Guardian review and carry that approval
    context into the next model turn.
    
    This PR does not add the actual `/auto-review-denials` tool 
    
    ## What Changed
    
    - Added app-server v2 RPC `thread/approveGuardianDeniedAction`.
    - Added generated JSON schema and TypeScript fixtures for
    `ThreadApproveGuardianDeniedAction*`.
    - Added core `Op::ApproveGuardianDeniedAction`.
    - Added a core handler that validates the event is a denied Guardian
    assessment and injects a developer message containing the stored denial
    event JSON.
    - Queues the approval context for the next turn if there is no active
    turn yet.
    - Added the TUI app-server bridge so `Op::ApproveGuardianDeniedAction {
    event }` is routed to the app-server request.
    
    ## What This Does Not Do
    
    - Does not add `/auto-review-denials`.
    - Does not add chat widget recent-denial state.
    - Does not add popup/list UI.
    - Does not add a product-facing denial lookup/store.
    - Does not change where Guardian denials are originally emitted or
    persisted.
    
    ## Verification
    
    - `cargo test -p codex-tui thread_approve_guardian_denied_action`
  • app-server: expose thread permission profiles (#18278)
    ## Why
    
    The `PermissionProfile` migration needs app-server clients to see the
    same constrained permission model that core is using at runtime. Before
    this PR, thread lifecycle responses only exposed the legacy
    `SandboxPolicy` shape, so clients still had to infer active permissions
    from sandbox fields. That makes downstream resume, fork, and override
    flows harder to make `PermissionProfile`-first.
    
    External sandbox policies are intentionally excluded from this canonical
    view. External enforcement cannot be round-tripped as a
    `PermissionProfile`, and exposing a lossy root-write profile would let
    clients accidentally change sandbox semantics if they echo the profile
    back later.
    
    ## What changed
    
    - Adds the app-server v2 `PermissionProfile` wire shape, including
    filesystem permissions and glob scan depth metadata.
    - Adds `PermissionProfileNetworkPermissions` so the profile response
    does not expose active network state through the older
    additional-permissions naming.
    - Returns `permissionProfile` from thread start, resume, and fork
    responses when the active sandbox can be represented as a
    `PermissionProfile`.
    - Keeps legacy `sandbox` in those responses for compatibility and
    documents `permissionProfile` as canonical when present.
    - Makes lifecycle `permissionProfile` nullable and returns `null` for
    `ExternalSandbox` to avoid exposing a lossy profile.
    - Regenerates the app-server JSON schema and TypeScript fixtures.
    
    ## Verification
    
    - `cargo test -p codex-app-server-protocol`
    - `cargo test -p codex-app-server
    thread_response_permission_profile_omits_external_sandbox --
    --nocapture`
    - `cargo check --tests -p codex-analytics -p codex-exec -p codex-tui`
    - `just fix -p codex-app-server-protocol -p codex-app-server -p
    codex-analytics -p codex-exec -p codex-tui`
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/18278).
    * #18279
    * __->__ #18278
  • feat: add explicit AgentIdentity auth mode (#18785)
    ## Summary
    
    This PR adds `CodexAuth::AgentIdentity` as an explicit auth mode.
    
    An AgentIdentity auth record is a standalone `auth.json` mode. When
    `AuthManager::auth().await` loads that mode, it registers one
    process-scoped task and stores it in runtime-only state on the auth
    value. Header creation stays synchronous after that because the task is
    initialized before callers receive the auth object.
    
    This PR also removes the old feature flag path. AgentIdentity is
    selected by explicit auth mode, not by a hidden flag or lazy mutation of
    ChatGPT auth records.
    
    Reference old stack: https://github.com/openai/codex/pull/17387/changes
    
    ## Design Decisions
    
    - AgentIdentity is a real auth enum variant because it can be the only
    credential in `auth.json`.
    - The process task is ephemeral runtime state. It is not serialized and
    is not stored in rollout/session data.
    - Account/user metadata needed by existing Codex backend checks lives on
    the AgentIdentity record for now.
    - `is_chatgpt_auth()` remains token-specific.
    - `uses_codex_backend()` is the broader predicate for ChatGPT-token auth
    and AgentIdentity auth.
    
    ## Stack
    
    1. https://github.com/openai/codex/pull/18757: full revert
    2. https://github.com/openai/codex/pull/18871: isolated Agent Identity
    crate
    3. This PR: explicit AgentIdentity auth mode and startup task allocation
    4. https://github.com/openai/codex/pull/18811: migrate Codex backend
    auth callsites through AuthProvider
    5. https://github.com/openai/codex/pull/18904: accept AgentIdentity JWTs
    and load `CODEX_AGENT_IDENTITY`
    
    ## Testing
    
    Tests: targeted Rust checks, cargo-shear, Bazel lock check, and CI.
  • Add turn-scoped environment selections (#18416)
    ## Summary
    - add experimental turn/start.environments params for per-turn
    environment id + cwd selections
    - pass selections through core protocol ops and resolve them with
    EnvironmentManager before TurnContext creation
    - treat omitted selections as default behavior, empty selections as no
    environment, and non-empty selections as first environment/cwd as the
    turn primary
    
    ## Testing
    - ran `just fmt`
    - ran `just write-app-server-schema`
    - not run: unit tests for this stacked PR
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Use app server metadata for fork parent titles (#18632)
    ## Problem
    The TUI resolved fork parent titles from local CODEX_HOME metadata,
    which could show missing or stale titles when app-server metadata is
    authoritative.
    
    This is a lingering bug left over from the migration of the TUI to the
    app-server interface. I found it when I asked Codex to review all places
    where the TUI code was still directly accessing the local CODEX_HOME.
    
    ## Solution
    Route fork parent title metadata through the app-server session state
    and render only that supplied title, with focused snapshot coverage for
    stale local metadata.
    
    ## Testing
    I manually tested by renaming a thread then forking it and confirming
    that the "forked from" message indicated the parent thread's name.
  • Add /side conversations (#18190)
    The TUI supports long-running turns and agent threads, but quick side
    questions have required interrupting the main flow or manually
    forking/navigating threads. This PR adds a guarded `/side` flow so users
    can ask brief side-conversation questions in an ephemeral fork while
    keeping the primary thread focused. This also helps address the feature
    request in #18125.
    
    The implementation creates one side conversation at a time, lets `/side`
    open either an empty side thread or immediately submit `/side
    <question>`, and returns to the parent with Esc or Ctrl+C. Side
    conversations get hidden developer guardrails that treat inherited
    history as reference-only and steer the model away from workspace
    mutations unless explicitly requested in the side conversation.
    
    The TUI hides most slash commands while side mode is active, leaving
    only `/copy`, `/diff`, `/mention`, and `/status` available there.
  • [TUI] add external config migration prompt when start TUI (#17891)
    - add a TUI startup migration prompt for external agent config
    - support migrating external configs including config, skills, AGENTS.md
    and plugins
    - gate the prompt behind features.external_migrate (default false)
    
    <img width="1037" height="480" alt="Screenshot 2026-04-14 at 9 29 14 PM"
    src="https://github.com/user-attachments/assets/6060849b-03cb-429a-9c13-c7bb46ad2e65"
    />
    <img width="713" height="183" alt="Screenshot 2026-04-14 at 9 29 26 PM"
    src="https://github.com/user-attachments/assets/d13f177e-d4c4-479c-8736-ef29636081e1"
    />
    
    ---------
    
    Co-authored-by: Eric Traut <etraut@openai.com>
  • [codex] Propagate rate limit reached type (#18227)
    ## Summary
    
    First PR in the split from #17956.
    
    - adds the core/app-server `RateLimitReachedType` shape
    - maps backend `rate_limit_reached_type` into Codex rate-limit snapshots
    - carries the field through app-server notifications/responses and
    generated schemas
    - updates existing constructors/tests for the new optional field
    
    ## Validation
    
    - `cargo test -p codex-backend-client`
    - `cargo test -p codex-app-server-protocol`
    - `cargo test -p codex-app-server rate_limits`
    - `cargo test -p codex-tui workspace_`
    - `cargo test -p codex-tui status_`
    - `just fmt`
    - `just fix -p codex-backend-client`
    - `just fix -p codex-app-server-protocol`
    - `just fix -p codex-app-server`
    - `just fix -p codex-tui`
  • [codex] Revoke ChatGPT tokens on logout (#17825)
    ## Summary
    
    This changes Codex logout so managed ChatGPT auth is revoked against
    AuthAPI before local auth state is removed. CLI logout, TUI `/logout`,
    and the app-server account logout path now use the token-revoking logout
    flow instead of only deleting `auth.json` / credential store state.
    
    ## Root Cause
    
    Logout previously cleared only local auth storage. That removed Codex's
    local credentials but did not ask the backend to invalidate the
    refresh/access token state associated with a managed ChatGPT login.
    
    ## Behavior
    
    For managed ChatGPT auth, logout sends the stored refresh token to
    `https://auth.openai.com/oauth/revoke` with `token_type_hint:
    refresh_token` and the Codex OAuth client id, then deletes all local
    auth stores after revocation succeeds. If only an access token is
    available, it falls back to revoking that access token. API key auth and
    externally supplied `chatgptAuthTokens` are still only cleared locally
    because Codex does not own a refresh token for those modes.
    
    Revocation failures are fail-closed: if Codex cannot load stored auth or
    the backend revoke call fails, logout returns an error and leaves local
    auth in place so the user can retry instead of silently clearing local
    state while backend tokens remain valid.
    
    ## Validation
    ran local version of `codex-cli` with staging overrides/harness for auth
    
    ran `codex login` then `codex logout`:
    
    saw auth.json clear and  backend revocation endpoints were called
    
    ```
    POST /oauth/revoke
    status: 200
    
    revoking access token
    should clear auth session
    clearing auth session due to token revocation
    successfully revoked session and access token
    CANONICAL-API-LINE Response: status='200' method='POST' path='/oauth/revoke
    ```
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Fix MCP startup cancellation through app server (#18078)
    Addresses https://github.com/openai/codex/issues/17143
    
    Problem: TUI interrupts without an active turn stopped cancelling slow
    MCP startup after routing through the app-server APIs.
    
    Solution: Route no-active-turn interrupts through app-server as startup
    cancels, acknowledge them immediately, and emit cancelled MCP startup
    updates.
    
    Testing: I manually confirmed that MCP cancellation didn't work prior to
    this PR and works after the fix was in place.
  • feat: reset memories button (#17937)
    <img width="720" height="175" alt="Screenshot 2026-04-15 at 14 35 02"
    src="https://github.com/user-attachments/assets/041d73ff-8c16-42a9-8e92-c245805084f0"
    />
  • feat: memories menu (#17632)
    Add menu that:
    1. If memories feature is not enabled, propose to enable it
    2. Let you choose if you want to generate memories and to use memories
  • Spread AbsolutePathBuf (#17792)
    Mechanical change to promote absolute paths through code.
  • Add realtime output modality and transcript events (#17701)
    - Add outputModality to thread/realtime/start and wire text/audio output
    selection through app-server, core, API, and TUI.\n- Rename the realtime
    transcript delta notification and add a separate transcript done
    notification that forwards final text from item done without correlating
    it with deltas.
  • [codex] Remove unused Rust helpers (#17146)
    ## Summary
    
    Removes high-confidence unused Rust helper functions and exports across
    `codex-tui`, `codex-shell-command`, and utility crates.
    
    The cleanup includes dead TUI helper methods, unused
    path/string/elapsed/fuzzy-match utilities, an unused Windows PowerShell
    lookup helper, and the unused terminal palette version counter. This
    keeps the remaining public surface smaller without changing behavior.
    
    ## Validation
    
    - `just fmt`
    - `cargo test -p codex-tui -p codex-shell-command -p codex-utils-elapsed
    -p codex-utils-fuzzy-match -p codex-utils-string -p codex-utils-path`
    - `just fix -p codex-tui -p codex-shell-command -p codex-utils-elapsed
    -p codex-utils-fuzzy-match -p codex-utils-string -p codex-utils-path`
    - `git diff --check`
  • Expose instruction sources (AGENTS.md) via app server (#17506)
    Addresses #17498
    
    Problem: The TUI derived /status instruction source paths from the local
    client environment, which could show stale <none> output or incorrect
    paths when connected to a remote app server.
    
    Solution: Add an app-server v2 instructionSources snapshot to thread
    start/resume/fork responses, default it to an empty list when older
    servers omit it, and render TUI /status from that server-provided
    session data.
    
    Additional context: The app-server field is intentionally named
    instructionSources rather than AGENTS.md-specific terminology because
    the loaded instruction sources can include global instructions, project
    AGENTS.md files, AGENTS.override.md, user-defined instruction files, and
    future dynamic sources.
  • TUI: enforce core boundary (#17399)
    Problem: The TUI still depended on `codex-core` directly in a number of
    places, and we had no enforcement from keeping this problem from getting
    worse.
    
    Solution: Route TUI core access through
    `codex-app-server-client::legacy_core`, add CI enforcement for that
    boundary, and re-export this legacy bridge inside the TUI as
    `crate::legacy_core` so the remaining call sites stay readable. There is
    no functional change in this PR — just changes to import targets.
    
    Over time, we can whittle away at the remaining symbols in this legacy
    namespace with the eventual goal of removing them all. In the meantime,
    this linter rule will prevent us from inadvertently importing new
    symbols from core.
  • Revert "Option to Notify Workspace Owner When Usage Limit is Reached" (#17391)
    Reverts openai/codex#16969
    
    #sev3-2026-04-10-accountscheckversion-500s-for-openai-workspace-7300
  • Support clear SessionStart source (#17073)
    ## Motivation
    
    The `SessionStart` hook already receives `startup` and `resume` sources,
    but sessions created from `/clear` previously looked like normal startup
    sessions. This makes it impossible for hook authors to distinguish
    between these with the matcher.
    
    ## Summary
    
    - Add `InitialHistory::Cleared` so `/clear`-created sessions can be
    distinguished from ordinary startup sessions.
    - Add `SessionStartSource::Clear` and wire it through core, app-server
    thread start params, and TUI clear-session flow.
    - Update app-server protocol schemas, generated TypeScript, docs, and
    related tests.
    
    
    https://github.com/user-attachments/assets/9cae3cb4-41c7-4d06-b34f-966252442e5c
  • Option to Notify Workspace Owner When Usage Limit is Reached (#16969)
    ## Summary
    - Replace the manual `/notify-owner` flow with an inline confirmation
    prompt when a usage-based workspace member hits a credits-depleted
    limit.
    - Fetch the current workspace role from the live ChatGPT
    `accounts/check/v4-2023-04-27` endpoint so owner/member behavior matches
    the desktop and web clients.
    - Keep owner, member, and spend-cap messaging distinct so we only offer
    the owner nudge when the workspace is actually out of credits.
    
    ## What Changed
    - `backend-client`
    - Added a typed fetch for the current account role from
    `accounts/check`.
      - Mapped backend role values into a Rust workspace-role enum.
    - `app-server` and protocol
      - Added `workspaceRole` to `account/read` and `account/updated`.
    - Derived `isWorkspaceOwner` from the live role, with a fallback to the
    cached token claim when the role fetch is unavailable.
    - `tui`
      - Removed the explicit `/notify-owner` slash command.
    - When a member is blocked because the workspace is out of credits, the
    error now prompts:
    - `Your workspace is out of credits. Request more from your workspace
    owner? [y/N]`
      - Choosing `y` sends the existing owner-notification request.
    - Choosing `n`, pressing `Esc`, or accepting the default selection
    dismisses the prompt without sending anything.
    - Selection popups now honor explicit item shortcuts, which is how the
    `y` / `n` interaction is wired.
    
    ## Reviewer Notes
    - The main behavior change is scoped to usage-based workspace members
    whose workspace credits are depleted.
    - Spend-cap reached should not show the owner-notification prompt.
    - Owners and admins should continue to see `/usage` guidance instead of
    the member prompt.
    - The live role fetch is best-effort; if it fails, we fall back to the
    existing token-derived ownership signal.
    
    ## Testing
    - Manual verification
      - Workspace owner does not see the member prompt.
    - Workspace member with depleted credits sees the confirmation prompt
    and can send the nudge with `y`.
    - Workspace member with spend cap reached does not see the
    owner-notification prompt.
    
    ### Workspace member out of usage
    
    https://github.com/user-attachments/assets/341ac396-eff4-4a7f-bf0c-60660becbea1
    
    ### Workspace owner
    <img width="1728" height="1086" alt="Screenshot 2026-04-09 at 11 48
    22 AM"
    src="https://github.com/user-attachments/assets/06262a45-e3fc-4cc4-8326-1cbedad46ed6"
    />
  • Forward app-server turn clientMetadata to Responses (#16009)
    ## Summary
    App-server v2 already receives turn-scoped `clientMetadata`, but the
    Rust app-server was dropping it before the outbound Responses request.
    This change keeps the fix lightweight by threading that metadata through
    the existing turn-metadata path rather than inventing a new transport.
    
    ## What we're trying to do and why
    We want turn-scoped metadata from the app-server protocol layer,
    especially fields like Hermes/GAAS run IDs, to survive all the way to
    the actual Responses API request so it is visible in downstream
    websocket request logging and analytics.
    
    The specific bug was:
    - app-server protocol uses camelCase `clientMetadata`
    - Responses transport already has an existing turn metadata carrier:
    `x-codex-turn-metadata`
    - websocket transport already rewrites that header into
    `request.request_body.client_metadata["x-codex-turn-metadata"]`
    - but the Rust app-server never parsed or stored `clientMetadata`, so
    nothing from the app-server request was making it into that existing
    path
    
    This PR fixes that without adding a new header or a second metadata
    channel.
    
    ## How we did it
    ### Protocol surface
    - Add optional `clientMetadata` to v2 `TurnStartParams` and
    `TurnSteerParams`
    - Regenerate the JSON schema / TypeScript fixtures
    - Update app-server docs to describe the field and its behavior
    
    ### Runtime plumbing
    - Add a dedicated core op for app-server user input carrying turn-scoped
    metadata: `Op::UserInputWithClientMetadata`
    - Wire `turn/start` and `turn/steer` through that op / signature path
    instead of dropping the metadata at the message-processor boundary
    - Store the metadata in `TurnMetadataState`
    
    ### Transport behavior
    - Reuse the existing serialized `x-codex-turn-metadata` payload
    - Merge the new app-server `clientMetadata` into that JSON additively
    - Do **not** replace built-in reserved fields already present in the
    turn metadata payload
    - Keep websocket behavior unchanged at the outer shape level: it still
    sends only `client_metadata["x-codex-turn-metadata"]`, but that JSON
    string now contains the merged fields
    - Keep HTTP fallback behavior unchanged except that the existing
    `x-codex-turn-metadata` header now includes the merged fields too
    
    ### Request shape before / after
    Before, a websocket `response.create` looked like:
    ```json
    {
      "type": "response.create",
      "client_metadata": {
        "x-codex-turn-metadata": "{\"session_id\":\"...\",\"turn_id\":\"...\"}"
      }
    }
    ```
    Even if the app-server caller supplied `clientMetadata`, it was not
    represented there.
    
    After, the same request shape is preserved, but the serialized payload
    now includes the new turn-scoped fields:
    ```json
    {
      "type": "response.create",
      "client_metadata": {
        "x-codex-turn-metadata": "{\"session_id\":\"...\",\"turn_id\":\"...\",\"fiber_run_id\":\"fiber-start-123\",\"origin\":\"gaas\"}"
      }
    }
    ```
    
    ## Validation
    ### Targeted tests added / updated
    - protocol round-trip coverage for `clientMetadata` on `turn/start` and
    `turn/steer`
    - protocol round-trip coverage for `Op::UserInputWithClientMetadata`
    - `TurnMetadataState` merge test proving client metadata is added
    without overwriting reserved built-in fields
    - websocket request-shape test proving outbound `response.create`
    contains merged metadata inside
    `client_metadata["x-codex-turn-metadata"]`
    - app-server integration tests proving:
    - `turn/start` forwards `clientMetadata` into the outbound Responses
    request path
      - websocket warmup + real turn request both behave correctly
      - `turn/steer` updates the follow-up request metadata
    
    ### Commands run
    - `just write-app-server-schema`
    - `cargo test -p codex-app-server-protocol`
    - `cargo test -p codex-protocol`
    - `cargo test -p codex-core
    turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields
    --lib`
    - `cargo test -p codex-core --test all
    responses_websocket_preserves_custom_turn_metadata_fields`
    - `cargo test -p codex-app-server --test all client_metadata`
    - `cargo test -p codex-app-server --test all
    turn_start_forwards_client_metadata_to_responses_websocket_request_body_v2
    -- --nocapture`
    - `just fmt`
    - `just fix -p codex-core -p codex-protocol -p codex-app-server-protocol
    -p codex-app-server`
    - `just fix -p codex-exec -p codex-tui-app-server`
    - `just argument-comment-lint`
    
    ### Full suite note
    `cargo test` in `codex-rs` still fails in:
    -
    `suite::v2::turn_interrupt::turn_interrupt_resolves_pending_command_approval_request`
    
    I verified that same failure on a clean detached `HEAD` worktree with an
    isolated `CARGO_TARGET_DIR`, so it is not caused by this patch.
  • Add realtime voice selection (#17176)
    - Add realtime voice selection for realtime/start.
    - Expose the supported v1/v2 voice lists and cover explicit, configured,
    default, and invalid voice paths.
  • fix(tui): reduce startup and new-session latency (#17039)
    ## TL;DR
    
    - Fetches account/rateLimits/read asynchronously so the TUI can continue
    starting without waiting for the rate-limit response.
    - Fixes the /status card so it no longer leaves a stale “refreshing
    cached limits...” notice in terminal history.
    
    ## Problem
    
    The TUI bootstrap path fetched account rate limits synchronously
    (`account/rateLimits/read`) before the event loop started for
    ChatGPT/OpenAI-authenticated startups. This added ~670 ms of blocking
    latency in the measured hot-start case, even though rate-limit data is
    not needed to render the initial UI or accept user input. The delay was
    especially noticeable on hot starts where every other RPC
    (`account/read`, `model/list`, `thread/start`) completed in under 70 ms
    total.
    
    Moving that fetch to the background also exposed a `/status` UI bug: the
    status card is flattened into terminal scrollback when it is inserted. A
    transient "refreshing limits in background..." line could not be cleared
    later, because the async completion updated the retained `HistoryCell`,
    not the already-written terminal history.
    
    ## Mental model
    
    Before this change, `AppServerSession::bootstrap()` performed three
    sequential RPCs: `account/read` → `model/list` →
    `account/rateLimits/read`. The result of the third call was baked into
    `AppServerBootstrap` and applied to the chat widget before the event
    loop began.
    
    After this change, `bootstrap()` only performs two RPCs (`account/read`
    + `model/list`), and rate-limit fetching is kicked off as an async
    background task immediately after the first frame is scheduled. A new
    enum, `RateLimitRefreshOrigin`, tags each fetch so the event handler
    knows whether the result came from the startup prefetch or from a
    user-initiated `/status` command; they have different completion
    side-effects.
    
    The `get_login_status()` helper (used outside the main app flow) was
    also decoupled: it previously called the full `bootstrap()` just to
    check auth mode, wasting model-list and rate-limit work. It now calls
    the narrower `read_account()` directly.
    
    For `/status`, this PR keeps the background refresh request but stops
    printing transient refresh notices into status history when cached
    limits are already available. If a refresh updates the cache, the next
    `/status` command will render the new values.
    
    ## Non-goals
    
    - This change does not alter the rate-limit data itself.
    - This change does not introduce caching, retries, or staleness
    management for rate limits.
    - This change does not affect the `model/list` or `thread/start` RPCs;
    they remain on the critical startup path.
    
    ## Tradeoffs
    
    - **Stale-on-first-render**: The status bar will briefly show no
    rate-limit info until the background fetch completes; observed
    background fetches landed roughly in the 400-900 ms range after the UI
    appeared. This is acceptable because the user cannot meaningfully act on
    rate-limit data in the first fraction of a second.
    - **Error silence on startup prefetch**: If the startup prefetch fails,
    the error is logged but the UI is not notified (unlike `/status` refresh
    failures, which go through the status-command completion path). This
    avoids surfacing transient network errors as a startup blocker.
    - **Static `/status` history**: `/status` output is terminal history,
    not a live widget. The card now avoids progress-style language that
    would appear stuck in scrollback; users can run `/status` again to see
    newly cached values.
    - **`account_auth_mode` field removed from `AppServerBootstrap`**: The
    only consumer was `get_login_status()`, which no longer goes through
    `bootstrap()`. The field was dead weight.
    
    ## Architecture
    
    ### New types
    
    - `RateLimitRefreshOrigin` (in `app_event.rs`): A `Copy` enum
    distinguishing `StartupPrefetch` from `StatusCommand { request_id }`.
    Carried through `RefreshRateLimits` and `RateLimitsLoaded` events so the
    handler applies the right completion behavior.
    
    ### Modified types
    
    - `AppServerBootstrap`: Lost `account_auth_mode` and
    `rate_limit_snapshots`; gained `requires_openai_auth: bool` (passed
    through from the account response so the caller can decide whether to
    fire the prefetch).
    
    ### Control flow
    
    1. `bootstrap()` returns with `requires_openai_auth` and
    `has_chatgpt_account`.
    2. After scheduling the first frame, `App::run_inner` fires
    `refresh_rate_limits(StartupPrefetch)` if both flags are true.
    3. When `RateLimitsLoaded { StartupPrefetch, Ok(..) }` arrives,
    snapshots are applied and a frame is scheduled to repaint the status
    bar.
    4. When `RateLimitsLoaded { StartupPrefetch, Err(..) }` arrives, the
    error is logged and no UI update occurs.
    5. `/status`-initiated refreshes continue to use `StatusCommand {
    request_id }` and call `finish_status_rate_limit_refresh` on completion
    (success or failure).
    6. `/status` history cells with cached rate-limit rows no longer render
    an additional "refreshing limits" notice; the async refresh updates the
    cache for future status output.
    
    ### Extracted method
    
    - `AppServerSession::read_account()`: Factored out of `bootstrap()` so
    that `get_login_status()` can call it independently without triggering
    model-list or rate-limit work.
    
    ## Observability
    
    - The existing `tracing::warn!` for rate-limit fetch failures is
    preserved for the startup path.
    - No new metrics or spans are introduced. The startup-time improvement
    is observable via the existing `ready` timestamp in TUI startup logs.
    
    ## Tests
    
    - Existing tests in `status_command_tests.rs` are updated to match on
    `RateLimitRefreshOrigin::StatusCommand { request_id }` instead of a bare
    `request_id`.
    - Focused `/status` tests now assert that status history avoids
    transient refresh text, continues to request an async refresh, and uses
    refreshed cached limits in future status output.
    - No new tests are added for the startup prefetch path because it is a
    fire-and-forget spawn with no observable side-effect other than the
    widget state update, which is already covered by the
    snapshot-application tests.
    
    ---------
    
    Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  • Use model metadata for Fast Mode status (#16949)
    Fast Mode status was still tied to one model name in the TUI and
    model-list plumbing. This changes the model metadata shape so a model
    can advertise additional speed tiers, carries that field through the
    app-server model list, and uses it to decide when to show Fast Mode
    status.
    
    For people using Codex, the behavior is intended to stay the same for
    existing models. Fast Mode still requires the existing signed-in /
    feature-gated path; the difference is that the UI can now recognize any
    model the model list marks as Fast-capable, instead of requiring a new
    client-side slug check.
  • Add WebRTC transport to realtime start (#16960)
    Adds WebRTC startup to the experimental app-server
    `thread/realtime/start` method with an optional transport enum. The
    websocket path remains the default; WebRTC offers create the realtime
    session through the shared start flow and emit the answer SDP via
    `thread/realtime/sdp`.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • [codex] reduce module visibility (#16978)
    ## Summary
    - reduce public module visibility across Rust crates, preferring private
    or crate-private modules with explicit crate-root public exports
    - update external call sites and tests to use the intended public crate
    APIs instead of reaching through module trees
    - add the module visibility guideline to AGENTS.md
    
    ## Validation
    - `cargo check --workspace --all-targets --message-format=short` passed
    before the final fix/format pass
    - `just fix` completed successfully
    - `just fmt` completed successfully
    - `git diff --check` passed
  • [codex-analytics] add protocol-native turn timestamps (#16638)
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/16638).
    * #16870
    * #16706
    * #16659
    * #16641
    * #16640
    * __->__ #16638
  • Add remote --cd forwarding for app-server sessions (#16700)
    Addresses #16124
    
    Problem: `codex --remote --cd <path>` canonicalized the path locally and
    then omitted it from remote thread lifecycle requests, so remote-only
    working directories failed or were ignored.
    
    Solution: Keep remote startup on the local cwd, forward explicit `--cd`
    values verbatim to `thread/start`, `thread/resume`, and `thread/fork`,
    and cover the behavior with `codex-tui` tests.
    
    Testing: I manually tested `--remote --cd` with both absolute and
    relative paths and validated correct behavior.
    
    
    ---
    
    Update based on code review feedback:
    
    Problem: Remote `--cd` was forwarded to `thread/resume` and
    `thread/fork`, but not to `thread/list` lookups, so `--resume --last`
    and picker flows could select a session from the wrong cwd; relative cwd
    filters also failed against stored absolute paths.
    
    Solution: Apply explicit remote `--cd` to `thread/list` lookups for
    `--last` and picker flows, normalize relative cwd filters on the
    app-server before exact matching, and document/test the behavior.
  • Fix fork source display in /status (expose forked_from_id in app server) (#16596)
    Addresses #16560
    
    Problem: `/status` stopped showing the source thread id in forked TUI
    sessions after the app-server migration.
    
    Solution: Carry fork source ids through app-server v2 thread data and
    the TUI session adapter, and update TUI fixtures so `/status` matches
    the old TUI behavior.
  • Fix stale /status rate limits in active TUI sessions (#16201)
    Fix stale weekly limit in `/status` (#16194): /status reused the
    session’s cached rate-limit snapshot, so the weekly remaining limit
    could stay frozen within an active session.
    
    With this change, we now dynamically update the rate limits after status
    is displayed.
    
    I needed to delete a few low-value test cases from the chatWidget tests
    because the test.rs file is really large, and the new tests in this PR
    pushed us over the 512K mandated limit. I'm working on a separate PR to
    refactor that test file.
  • Rename tui_app_server to tui (#16104)
    This is a follow-up to https://github.com/openai/codex/pull/15922. That
    previous PR deleted the old `tui` directory and left the new
    `tui_app_server` directory in place. This PR renames `tui_app_server` to
    `tui` and fixes up all references.