2053 Commits

  • Add safety check notification and error handling (#19055)
    Adds a new app-server notification that fires when a user account has
    been flagged for potential safety reasons.
  • Default Fast service tier for eligible ChatGPT plans (#19053)
    ## Why
    
    Enterprise and business-like ChatGPT plans should get Codex's Fast
    service tier by default when the user or caller has not made an explicit
    service-tier choice. At the same time, callers need a durable way to
    choose standard routing without adding a new persisted `standard`
    service tier value. This keeps existing config compatibility while
    letting core own the managed default policy.
    
    ## What changed
    
    - Resolve the effective service tier in core at session creation:
    explicit `fast` or `flex` wins, explicit null/clear or
    `[notice].fast_default_opt_out = true` resolves to standard routing, and
    otherwise eligible ChatGPT plans resolve to Fast when FastMode is
    enabled.
    - Add `[notice].fast_default_opt_out` as the persisted opt-out marker
    for managed Fast defaults.
    - Treat app-server/TUI `service_tier: null` as an explicit
    standard/clear choice by preserving that intent through config loading.
    - Update TUI rendering to use core's effective service tier for startup
    and status surfaces while still keeping `config.service_tier` as the
    explicit configured choice.
    - Update `/fast off` to clear `service_tier`, persist the opt-out
    marker, and send explicit standard for subsequent turns.
    
    ## Verification
    
    - Added unit coverage for config override/notice handling, service-tier
    resolution, runtime null clearing, and `/fast off` turn propagation.
    - `cargo build -p codex-cli`
    
    Full test suite was not run locally per author request.
  • protocol: report session permission profiles (#18282)
    ## Why
    
    Clients that observe `SessionConfigured` need the same canonical
    permission view that app-server thread responses provide. Reporting the
    profile in protocol events lets clients keep their local state
    synchronized without reinterpreting legacy sandbox fields.
    
    ## What changed
    
    This adds `permission_profile` to `SessionConfigured` and propagates it
    through core, exec JSON output, MCP server messages, and TUI
    history/widget handling.
    
    ## Verification
    
    - `cargo test -p codex-tui permissions -- --nocapture`
    - `cargo test -p codex-core --test all permissions_messages --
    --nocapture`
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/18282).
    * #18288
    * #18287
    * #18286
    * #18285
    * #18284
    * #18283
    * __->__ #18282
  • codex: support hooks in config.toml and requirements.toml (#18893)
    ## Summary
    
    Support the existing hooks schema in inline TOML so hooks can be
    configured from both `config.toml` and enterprise-managed
    `requirements.toml` without requiring a separate `hooks.json` payload.
    
    This gives enterprise admins a way to ship managed hook policy through
    the existing requirements channel while still leaving script delivery to
    MDM or other device-management tooling, and it keeps `hooks.json`
    working unchanged for existing users.
    
    This also lays the groundwork for follow-on managed filtering work such
    as #15937, while continuing to respect project trust gating from #14718.
    It does **not** implement `allow_managed_hooks_only` itself.
    
    NOTE: yes, it's a bit unfortunate that the toml isn't formatted as
    closely as normal to our default styling. This is because we're trying
    to stay compatible with the spec for plugins/hooks that we'll need to
    support & the main usecase here is embedding into requirements.toml
    
    ## What changed
    
    - moved the shared hook serde model out of `codex-rs/hooks` into
    `codex-rs/config` so the same schema can power `hooks.json`, inline
    `config.toml` hooks, and managed `requirements.toml` hooks
    - added `hooks` support to both `ConfigToml` and
    `ConfigRequirementsToml`, including requirements-side `managed_dir` /
    `windows_managed_dir`
    - treated requirements-managed hooks as one constrained value via
    `Constrained`, so managed hook policy is merged atomically and cannot
    drift across requirement sources
    - updated hook discovery to load requirements-managed hooks first, then
    per-layer `hooks.json`, then per-layer inline TOML hooks, with a warning
    when a single layer defines both representations
    - threaded managed hook metadata through discovered handlers and exposed
    requirements hooks in app-server responses, generated schemas, and
    `/debug-config`
    - added hook/config coverage in `codex-rs/config`, `codex-rs/hooks`,
    `codex-rs/core/src/config_loader/tests.rs`, and
    `codex-rs/core/tests/suite/hooks.rs`
    
    ## Testing
    
    - `cargo test -p codex-config`
    - `cargo test -p codex-hooks`
    - `cargo test -p codex-app-server config_api`
    
    ## Documentation
    
    Companion updates are needed in the developers website repo for:
    
    - the hooks guide
    - the config reference, sample, basic, and advanced pages
    - the enterprise managed configuration guide
    
    ---------
    
    Co-authored-by: Michael Bolin <mbolin@openai.com>
  • tui: fix approvals popup disabled shortcut test (#19072)
    ## Why
    
    This regressed in #19063, which made `GuardianApproval` stable and
    enabled by default. That adds an enabled `Auto-review` row to the
    permissions popup, but `approvals_popup_navigation_skips_disabled` still
    assumed the disabled `Full Access` row lived behind a hard-coded numeric
    shortcut, so the test started selecting a different row and closing the
    popup instead of verifying disabled-row behavior.
    
    ## What
    
    - disable `GuardianApproval` in
    `approvals_popup_navigation_skips_disabled` so the popup layout matches
    the scenario the test is exercising
    - choose the hidden numeric shortcut for the disabled `Full Access` row
    by platform (`2` on non-Windows, `3` on Windows where `Read Only` is
    shown) before asserting that selecting the disabled row leaves the popup
    open
    
    ## Testing
    
    - `cargo test -p codex-tui --lib
    chatwidget::tests::permissions::approvals_popup_navigation_skips_disabled
    -- --exact --nocapture`
    - `cargo test -p codex-tui --lib chatwidget::tests::permissions --
    --nocapture`
    - `cargo test -p codex-tui`
  • feat(request-permissions) approve with strict review (#19050)
    ## Summary
    Allow the user to approve a request_permissions_tool request with the
    condition that all commands in the rest of the turn are reviewed by
    guardian, regardless of sandbox status.
    
    ## Testing
    - [x] Added unit tests
    - [x] Ran locally
  • chore(auto-review) feature => stable (#19063)
    ## Summary
    Turn on Auto Review
    
    ## Testing
    - [x] Update unit tests
  • Rename approvals reviewer variant to auto-review (#19056)
    ## Why
    
    `approvals_reviewer` now uses `auto_review` as the canonical config/API
    value after #18504, but the Rust enum variant and nearby helper/test
    names still used `GuardianSubagent` / guardian approval wording. That
    made follow-up code and reviews confusing even though the external value
    had already moved to Auto-review.
    
    ## What changed
    
    - Renamed `ApprovalsReviewer::GuardianSubagent` to
    `ApprovalsReviewer::AutoReview`.
    - Updated protocol, app-server, config, core, TUI, exec, and analytics
    test callsites.
    - Renamed nearby helper/test names from guardian approval wording to
    Auto-review wording where they refer to the approvals reviewer mode.
    - Preserved wire compatibility:
      - `auto_review` remains the canonical serialized value.
      - `guardian_subagent` remains accepted as a legacy alias.
    
    This intentionally does not rename the `[features].guardian_approval`
    key, `Feature::GuardianApproval`, `core/src/guardian`, analytics event
    names, or app-server Guardian review event types.
    
    ## Verification
    
    - `cargo test -p codex-protocol
    approvals_reviewer_serializes_auto_review_and_accepts_legacy_guardian_subagent`
    - `cargo test -p codex-app-server-protocol
    approvals_reviewer_serializes_auto_review_and_accepts_legacy_guardian_subagent`
    - `cargo test -p codex-config approvals_reviewer`
    - `cargo test -p codex-tui update_feature_flags`
    - `cargo test -p codex-core permissions_instructions`
    - `cargo test -p codex-tui permissions_selection`
  • rollout: persist turn permission profiles (#18281)
    ## Why
    
    Resume and reconstruction need to preserve the permissions that were
    active for each user turn. If rollouts only keep legacy sandbox fields,
    replay cannot faithfully represent profile-shaped overrides introduced
    earlier in the stack.
    
    ## What changed
    
    This records `permission_profile` on user-turn rollout events,
    reconstructs it through history/state extraction, and updates rollout
    reconstruction and related fixtures to keep the field explicit.
    
    ## Verification
    
    - `cargo test -p codex-core --test all permissions_messages --
    --nocapture`
    - `cargo test -p codex-core --test all request_permissions --
    --nocapture`
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/18281).
    * #18288
    * #18287
    * #18286
    * #18285
    * #18284
    * #18283
    * #18282
    * __->__ #18281
  • 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
  • Rebrand approvals reviewer config to auto-review (#18504)
    ### Why
    
    Auto-review is the user-facing name for the approvals reviewer, but the
    config/API value still exposed the old `guardian_subagent` name. That
    made new configs and generated schemas point users at Guardian
    terminology even though the intended product surface is Auto-review.
    
    This PR updates the external `approvals_reviewer` value while preserving
    compatibility for existing configs and clients.
    
    ### What changed
    
    - Makes `auto_review` the canonical serialized value for
    `approvals_reviewer`.
    - Keeps `guardian_subagent` accepted as a legacy alias.
    - Keeps `user` accepted and serialized as `user`.
    - Updates generated config and app-server schemas so
    `approvals_reviewer` includes:
      - `user`
      - `auto_review`
      - `guardian_subagent`
    - Updates app-server README docs for the reviewer value.
    - Updates analytics and config requirements tests for the canonical
    auto_review value.
    
    
    ### Compatibility
    
    Existing configs and API payloads using:
    
    ```toml
    approvals_reviewer = "guardian_subagent"
    ```
    
    continue to load and map to the Auto-review reviewer behavior. 
    
    New serialization emits: 
    ```toml
    approvals_reviewer = "auto_review" 
    ```
    
    This PR intentionally does not rename the [features].guardian_approval
    key or broad internal Guardian symbols. Those are split out for a
    follow-up PR to keep this migration small and avoid touching large
    TUI/internal surfaces.
    
    **Verification**
    cargo test -p codex-protocol
    approvals_reviewer_serializes_auto_review_and_accepts_legacy_guardian_subagent
    cargo test -p codex-app-server-protocol
    approvals_reviewer_serializes_auto_review_and_accepts_legacy_guardian_subagent
  • 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
  • feat(auto-review) short-circuit (#18890)
    ## Summary
    Short circuit the convo if auto-review hits too many denials
    
    ## Testing
    - [x] Added unit tests
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • feat: Fairly trim skill descriptions within context budget (#18925)
    Preserve skill name/path entries whenever possible and trim descriptions
    first, using round-robin character allocation so short descriptions do
    not waste budget.
  • 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`
  • Support multiple cwd filters for thread list (#18502)
    ## Summary
    
    - Teach app-server `thread/list` to accept either a single `cwd` or an
    array of cwd filters, returning threads whose recorded session cwd
    matches any requested path
    - Add `useStateDbOnly` as an explicit opt-in fast path for callers that
    want to answer `thread/list` from SQLite without scanning JSONL rollout
    files
    - Preserve backwards compatibility: by default, `thread/list` still
    scans JSONL rollouts and repairs SQLite state
    - Wire the new cwd array and SQLite-only options through app-server,
    local/remote thread-store, rollout listing, generated TypeScript/schema
    fixtures, proto output, and docs
    
    ## Test Plan
    
    - `cargo test -p codex-app-server-protocol`
    - `cargo test -p codex-rollout`
    - `cargo test -p codex-thread-store`
    - `cargo test -p codex-app-server thread_list`
    - `just fmt`
    - `just fix -p codex-app-server-protocol -p codex-rollout -p
    codex-thread-store -p codex-app-server`
    - `cargo build -p codex-cli --bin codex`
  • [codex-analytics] guardian review TTFT plumbing and emission (#17696)
    ## Why
    
    Guardian analytics includes time-to-first-token, but the Guardian
    reviewer runs as a normal Codex session and `TurnCompleteEvent` did not
    expose TTFT. The timing needs to flow through the standard
    turn-completion protocol so Guardian review analytics can consume the
    same value as the rest of the session machinery.
    
    ## What changed
    
    Adds optional `time_to_first_token_ms` to `TurnCompleteEvent` and
    populates it from `TurnTiming`. The value is carried through app-server
    thread history, rollout reconstruction, TUI/app-server adapters, and
    Guardian review session handling.
    
    Guardian review analytics now captures TTFT from the reviewer
    turn-complete event when available. Existing tests and fixtures are
    updated to set the new optional field to `None` where TTFT is not
    relevant.
    
    ## Verification
    
    - `cargo clippy -p codex-tui --tests -- -D warnings`
    - `cargo clippy -p codex-core --lib --tests -- -D warnings`
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/17696).
    * __->__ #17696
    * #17695
    * #17693
    * #18278
    * #18953
  • 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.
  • chore(tui) debug-config guardian_policy_config (#18923)
    ## Summary
    List guardian_policy_config_source in `/debug-config` output
    
    ## Testing
     - [x] Ran locally
  • feat: Support remote plugin list/read. (#18452)
    Add a temporary internal remote_plugin feature flag that merges remote
    marketplaces into plugin/list and routes plugin/read through the remote
    APIs when needed, while keeping pure local marketplaces working as
    before.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • 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>
  • fix: windows snapshot for external_agent_config_migration::tests::prompt_snapshot did not match windows output (#18915)
    Fix a snapshot test that is failing on Windows, but is currently missed
    by Bazel due to https://github.com/openai/codex/pull/18913. We see this
    failing on Cargo builds on Windows, though.
    
    This Bazel vs. Cargo inconsistency explains why
    https://github.com/openai/codex/pull/18768 did not fix the Cargo Windows
    build.
  • Update /statusline and /title snapshots (#18909)
    Update `/statusline` and `/title` snapshots
  • [codex] Tighten external migration prompt tests (#18768)
    ## Summary
    - tighten the external migration prompt snapshot around stable synthetic
    fixture text
    - add focused display_description tests for relative path rewriting and
    plugin summaries
    - split the path-format assertions into smaller, easier-to-read unit
    tests
    
    ## Why
    The previous prompt snapshot was coupled to path text that came from
    detected migration items, which made it noisier and more brittle than
    necessary. This change keeps the snapshot focused on stable UI structure
    and moves dynamic path formatting checks into targeted unit tests.
    
    ## Validation
    - cargo test -p codex-tui external_agent_config_migration::tests::
    - cargo test -p codex-tui
    external_agent_config_migration::tests::display_description_
    - just fmt
    
    ## Notes
    Per the repo instructions, I did not rerun tests after the final `just
    fmt` pass.
  • Normalize /statusline & /title items (#18886)
    This change aligns the `/statusline` and `/title` UIs around the same
    normalized item model so both surfaces use consistent ids, labels, and
    preview semantics. It keeps the shared preview work from #18435 ,
    tightens the remaining mismatches by standardizing item naming, expands
    title/status item coverage where appropriate, and makes `/title` preview
    use the same title-specific formatting path as the real rendered
    terminal title.
    
    - Normalizes persisted item ids and keeps legacy aliases for
    compatibility
    - Aligns `status-line` and `terminal-title` items with the shared
    preview model
    - Routes `terminal-title` preview through title-specific formatting and
    truncation
    - Updates the affected status/title setup snapshots
    
    Added to `/statusline`:
    - status
    - task-progress
      
    Normalized in `/statusline`:
    - model-name -> model
    - project-root -> project-name
    
    Added to `/title`:
    - current-dir
    - context-remaining
    - context-used
    - five-hour-limit
    - weekly-limit
    - codex-version
    - used-tokens
    - total-input-tokens
    - total-output-tokens
    - session-id
    - fast-mode
    - model-with-reasoning
    
    Normalized in `/title`:
    - project -> project-name
    - thread -> thread-title
    - model-name -> model
  • Support multiple managed environments (#18401)
    ## Summary
    - refactor EnvironmentManager to own keyed environments with
    default/local lookup helpers
    - keep remote exec-server client creation lazy until exec/fs use
    - preserve disabled agent environment access separately from internal
    local environment access
    
    ## Validation
    - not run (per Codex worktree instruction to avoid tests/builds unless
    requested)
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • fix: fully revert agent identity runtime wiring (#18757)
    ## Summary
    
    This PR fully reverts the previously merged Agent Identity runtime
    integration from the old stack:
    https://github.com/openai/codex/pull/17387/changes
    
    It removes the Codex-side task lifecycle wiring, rollout/session
    persistence, feature flag plumbing, lazy `auth.json` mutation,
    background task auth paths, and request callsite changes introduced by
    that stack.
    
    This leaves the repo in a clean pre-AgentIdentity integration state so
    the follow-up PRs can reintroduce the pieces in smaller reviewable
    layers.
    
    ## Stack
    
    1. This PR: full revert
    2. https://github.com/openai/codex/pull/18871: move Agent Identity
    business logic into a crate
    3. https://github.com/openai/codex/pull/18785: add explicit
    AgentIdentity auth mode and startup task allocation
    4. https://github.com/openai/codex/pull/18811: migrate auth callsites
    through AuthProvider
    
    ## Testing
    
    Tests: targeted Rust checks, cargo-shear, Bazel lock check, and CI.
  • feat(tui): shortcuts to change reasoning level temporarily (#18866)
    ## Summary
    
    Adds main-chat shortcuts for changing reasoning effort one step at a
    time:
    
    - `Alt+,` lowers reasoning (has the `<` arrow on the key)
    - `Alt+.` raises reasoning (similarly, has the `>` arrow)
    
    The shortcut updates the active session only. It does not persist the
    selected reasoning level as the default for future sessions. In Plan
    mode, it applies temporarily to Plan mode without opening the
    global-vs-Plan scope prompt.
    
    ## Details
    
    The shortcut uses the active model preset to decide which reasoning
    levels are valid. If the current session has no explicit reasoning
    effort, it starts from the model default. Each keypress moves to the
    next supported level in the requested direction.
    
    The shortcut only runs from the main chat surface. If a popup or modal
    is open, input remains owned by that UI.
    
    In Plan mode, the shortcut updates the in-memory Plan reasoning override
    directly. The model/reasoning picker still keeps the existing scope
    prompt for explicit picker changes.
    
    ## Notes
    
    Ctrl-plus and Ctrl-minus were considered, but terminals do not deliver
    those combinations consistently, so this PR uses Alt shortcuts instead.
    
    If the current effort is unsupported by the selected model, the shortcut
    skips to the nearest supported level in the requested direction. If
    there is no valid step, it shows the existing boundary message.
    
    ## Tests
    
    - `cargo test -p codex-tui reasoning_shortcuts`
    - `cargo test -p codex-tui reasoning_effort`
    - `cargo test -p codex-tui reasoning_shortcut`
    - `cargo test -p codex-tui footer_snapshots`
    - `cargo test -p codex-tui`
    - `just fix -p codex-tui`
    - `./tools/argument-comment-lint/run.py -p codex-tui -- --tests`
    
    ---------
    
    Co-authored-by: Eric Traut <etraut@openai.com>
  • sandboxing: intersect permission profiles semantically (#18275)
    ## Why
    
    Permission approval responses must not be able to grant more access than
    the tool requested. Moving this flow to `PermissionProfile` means the
    comparison must be profile-shaped instead of `SandboxPolicy`-shaped, and
    cwd-relative special paths such as `:cwd` and `:project_roots` must stay
    anchored to the turn that produced the request.
    
    ## What changed
    
    This implements semantic `PermissionProfile` intersection in
    `codex-sandboxing` for file-system and network permissions. The
    intersection accepts narrower path grants, rejects broader grants,
    preserves deny-read carve-outs and glob scan depth, and materializes
    cwd-dependent special-path grants to absolute paths before they can be
    recorded for reuse.
    
    The request-permissions response paths now use that intersection
    consistently. App-server captures the request turn cwd before waiting
    for the client response, includes that cwd in the v2 approval params,
    and core stores the requested profile plus cwd for direct TUI/client
    responses and Guardian decisions before recording turn- or
    session-scoped grants. The TUI app-server bridge now preserves the
    app-server request cwd when converting permission approval params into
    core events.
    
    ## Verification
    
    - `cargo test -p codex-sandboxing intersect_permission_profiles --
    --nocapture`
    - `cargo test -p codex-app-server request_permissions_response --
    --nocapture`
    - `cargo test -p codex-core
    request_permissions_response_materializes_session_cwd_grants_before_recording
    -- --nocapture`
    - `cargo check -p codex-tui --tests`
    - `cargo check --tests`
    - `cargo test -p codex-tui
    app_server_request_permissions_preserves_file_system_permissions`
  • Move TUI app tests to modules they cover (#18799)
    ## Summary
    
    The TUI app refactor in #18753 moved the old `app.rs` tests into a
    single `app/tests.rs` file. That kept the split mechanically simple, but
    it left several focused unit tests far from the modules they exercise.
    
    This PR is a follow-up that moves tests next to the code they cover.
    
    It also adds `tui/src/app/test_support.rs` for shared fixture
    construction.
    
    This is just a mechanical refactoring (no functional changes) and does
    not affect any production code.
  • Queue follow-up input during user shell commands (#18820)
    Fixes #17954.
    
    ## Why
    When a manual shell command like `!sleep 10` is running, submitting
    plain text such as `hi` currently sends that text as a steer for the
    active shell turn. User shell turns are not steerable like model turns,
    so the TUI can remain stuck in `Working` after the shell command
    finishes.
    
    ## What Changed
    - Detect when the only active work is one or more
    `ExecCommandSource::UserShell` commands.
    - Queue plain submitted input in that state so it drains after the shell
    command and shell turn complete.
    - Preserve `!cmd` submissions during running work so explicit shell
    commands keep their existing behavior.
    - Add regression coverage for the `!sleep 10` plus `hi` flow in
    `chatwidget::tests::exec_flow::user_message_during_user_shell_command_is_queued_not_steered`.
    
    ## Verification
    - Manually confirmed hang before the fix and no hang after the fix
  • [codex] Add tmux-aware OSC 9 notifications (#17836)
    ## Summary
    - wrap OSC 9 notifications in tmux's DCS passthrough so terminal
    notifications make it through tmux
    - use codex-terminal-detection for OSC 9 auto-selection so tmux sessions
    inherit the underlying client terminal support
    - add focused notification backend tests for plain OSC 9 and
    tmux-wrapped output
    
    ## Stack
    - base PR: #18479
    - review order: #18479, then this PR
    
    ## Why
    Tmux does not forward OSC 9 notifications directly; the sequence has to
    be wrapped in tmux's DCS passthrough envelope. Codex also had local
    notification heuristics that could miss supported terminals when running
    under tmux, even though codex-terminal-detection already knows how to
    attribute tmux sessions to the client terminal.
    
    ## Validation
    - `just fmt`
    - `cargo test -p codex-tui` *(currently blocked by an unrelated existing
    compile error in `app-server/src/message_processor.rs:754` referencing
    `connection_id` out of scope; not caused by this change)*
    
    Co-authored-by: Codex <noreply@openai.com>
  • fix(tui): disable enhanced keys for VS Code WSL (#18741)
    Fixes https://github.com/openai/codex/issues/13638
    
    ## Why
    
    VS Code's integrated terminal can run a Linux shell through WSL without
    exposing `TERM_PROGRAM` to the Linux process, and with crossterm
    keyboard enhancement flags enabled that environment can turn dead-key
    composition into malformed key events instead of composed Unicode input.
    Codex already handles composed Unicode correctly, so the fix is to avoid
    enabling the terminal mode that breaks this path for the affected
    terminal combination.
    
    ## What Changed
    
    - Automatically skip crossterm keyboard enhancement flags when Codex
    detects WSL plus VS Code, including a Windows-side `TERM_PROGRAM` probe
    through WSL interop.
    - Add `CODEX_TUI_DISABLE_KEYBOARD_ENHANCEMENT` so users can
    force-disable or force-enable the keyboard enhancement policy for
    diagnosis.
    
    ## Verification
    
    - Added unit coverage for env parsing, VS Code detection, and the WSL/VS
    Code auto-disable policy.
    - `cargo check -p codex-tui` passed.
    - `./tools/argument-comment-lint/run.py -p codex-tui -- --tests` passed.
    - `cargo test -p codex-tui` was attempted locally, but the checkout
    failed during linking before tests executed because V8 symbols from
    `codex-code-mode` were unresolved for `arm64`.
  • show bash mode in the TUI (#18271)
    ## What
    
    - Explicitly show our "bash mode" by changing the color and adding a
    callout similar to how we do for `Plan mode (shift + tab to cycle)`
    - Also replace our `›` composer prefix with a bang `!`
    
    
    ![](https://github.com/user-attachments/assets/f5549c75-3a03-433d-aa57-e4c6d0682c49)
    
    ## Why
    
    - It was unclear that we had a Bash mode
    - This feels more responsive
    - It looks cool!
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • [tool search] support namespaced deferred dynamic tools (#18413)
    Deferred dynamic tools need to round-trip a namespace so a tool returned
    by `tool_search` can be called through the same registry key that core
    uses for dispatch.
    
    This change adds namespace support for dynamic tool specs/calls,
    persists it through app-server thread state, and routes dynamic tool
    calls by full `ToolName` while still sending the app the leaf tool name.
    Deferred dynamic tools must provide a namespace; non-deferred dynamic
    tools may remain top-level.
    
    It also introduces `LoadableToolSpec` as the shared
    function-or-namespace Responses shape used by both `tool_search` output
    and dynamic tool registration, so dynamic tools use the same wrapping
    logic in both paths.
    
    Validation:
    - `cargo test -p codex-tools`
    - `cargo test -p codex-core tool_search`
    
    ---------
    
    Co-authored-by: Sayan Sisodiya <sayan@openai.com>
  • Add remote_sandbox_config to our config requirements (#18763)
    ## Why
    
    Customers need finer-grained control over allowed sandbox modes based on
    the host Codex is running on. For example, they may want stricter
    sandbox limits on devboxes while keeping a different default elsewhere.
    
    Our current cloud requirements can target user/account groups, but they
    cannot vary sandbox requirements by host. That makes remote development
    environments awkward because the same top-level `allowed_sandbox_modes`
    has to apply everywhere.
    
    ## What
    
    Adds a new `remote_sandbox_config` section to `requirements.toml`:
    
    ```toml
    allowed_sandbox_modes = ["read-only"]
    
    [[remote_sandbox_config]]
    hostname_patterns = ["*.org"]
    allowed_sandbox_modes = ["read-only", "workspace-write"]
    
    [[remote_sandbox_config]]
    hostname_patterns = ["*.sh", "runner-*.ci"]
    allowed_sandbox_modes = ["read-only", "danger-full-access"]
    ```
    
    During requirements resolution, Codex resolves the local host name once,
    preferring the machine FQDN when available and falling back to the
    cleaned kernel hostname. This host classification is best effort rather
    than authenticated device proof.
    
    Each requirements source applies its first matching
    `remote_sandbox_config` entry before it is merged with other sources.
    The shared merge helper keeps that `apply_remote_sandbox_config` step
    paired with requirements merging so new requirements sources do not have
    to remember the extra call.
    
    That preserves source precedence: a lower-precedence requirements file
    with a matching `remote_sandbox_config` cannot override a
    higher-precedence source that already set `allowed_sandbox_modes`.
    
    This also wires the hostname-aware resolution through app-server,
    CLI/TUI config loading, config API reads, and config layer metadata so
    they all evaluate remote sandbox requirements consistently.
    
    ## Verification
    
    - `cargo test -p codex-config remote_sandbox_config`
    - `cargo test -p codex-config host_name`
    - `cargo test -p codex-core
    load_config_layers_applies_matching_remote_sandbox_config`
    - `cargo test -p codex-core
    system_remote_sandbox_config_keeps_cloud_sandbox_modes`
    - `cargo test -p codex-config`
    - `cargo test -p codex-core` unit tests passed; `tests/all.rs`
    integration matrix was intentionally stopped after the relevant focused
    tests passed
    - `just fix -p codex-config`
    - `just fix -p codex-core`
    - `cargo check -p codex-app-server`
  • feat(auto-review) Handle request_permissions calls (#18393)
    ## Summary
    When auto-review is enabled, it should handle request_permissions tool.
    We'll need to clean up the UX but I'm planning to do that in a separate
    pass
    
    ## Testing
    - [x] Ran locally
    <img width="893" height="396" alt="Screenshot 2026-04-17 at 1 16 13 PM"
    src="https://github.com/user-attachments/assets/4c045c5f-1138-4c6c-ac6e-2cb6be4514d8"
    />
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Fallback display names for TUI skill mentions (#18786)
    This updates TUI skill mentions to show a fallback label when a skill
    does not define a display name, so unnamed skills remain understandable
    in the picker without changing behavior for skills that already have
    one.
    
    <img width="1028" height="198" alt="Screenshot 2026-04-20 at 6 25 15 PM"
    src="https://github.com/user-attachments/assets/84077b85-99d0-4db9-b533-37e1887b4506"
    />
  • protocol: preserve glob scan depth in permission profiles (#18713)
    ## Why
    
    #18274 made `PermissionProfile` the canonical file-system permissions
    shape, but the round-trip from `FileSystemSandboxPolicy` to
    `PermissionProfile` still dropped one piece of policy metadata:
    `glob_scan_max_depth`.
    
    That field is security-relevant for deny-read globs such as `**/*.env`.
    On Linux, bubblewrap sandbox construction uses it to bound unreadable
    glob expansion. If a profile copied from active runtime permissions
    loses this value and is submitted back as an override, the resulting
    `FileSystemSandboxPolicy` can behave differently even though the visible
    permission entries look equivalent.
    
    ## What changed
    
    - Add `glob_scan_max_depth` to protocol `FileSystemPermissions` and
    preserve it when converting to/from `FileSystemSandboxPolicy`.
    - Keep legacy `read`/`write` JSON for simple path-only permissions, but
    force canonical JSON when glob scan depth is present so the metadata is
    not silently dropped.
    - Carry `globScanMaxDepth` through app-server
    `AdditionalFileSystemPermissions`, generated JSON/TypeScript schemas,
    and app-server/TUI conversion call sites.
    - Preserve the metadata through sandboxing permission normalization,
    merging, and intersection.
    - Carry the merged scan depth into the effective
    `FileSystemSandboxPolicy` used for command execution, so bounded
    deny-read globs reach Linux bubblewrap materialization.
    
    ## Verification
    
    - `cargo test -p codex-sandboxing glob_scan -- --nocapture`
    - `cargo test -p codex-sandboxing policy_transforms -- --nocapture`
    - `just fix -p codex-sandboxing`
    
    
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/18713).
    * #18288
    * #18287
    * #18286
    * #18285
    * #18284
    * #18283
    * #18282
    * #18281
    * #18280
    * #18279
    * #18278
    * #18277
    * #18276
    * #18275
    * __->__ #18713
  • /statusline & /title - Shared preview values (#18435)
    This PR makes the `/statusline` and `/title` setup UIs share one
    preview-value source instead of each surface using its own examples.
    Both pickers now render consistent live values when available, and
    stable placeholders when they are not. It also resolves live preview
    values at the shared preview-item layer, so `/title` preview can use
    real runtime values for title-specific cases like status text, task
    progress, and project-name fallback behavior.
    
    - Adds a shared preview data model for status surfaces
    - Maps status-line items and terminal-title items onto that shared
    preview list
    - Feeds both setup views from the same chatwidget-derived preview data,
    with terminal-title-specific formatting applied before `/title` preview
    renders
    - Keeps project-root preview aligned with status-line behavior while
    project in /title keeps its title fallback/truncation behavior
    - Adds snapshot coverage for live-only, hardcoded-only, and mixed cases
    
    Test Steps
    - Open Codex TUI and launch `/statusline`.
    - Toggle and reorder items, then verify the preview uses current session
    values when possible, and placeholder values for missing values (ex: no
    thread ID).
    - Open `/title` and verify it shows the same normalized values,
    including live status/task-progress values when available.
  • Warn when trusting Git subdirectories (#18602)
    Addresses #18505
    
    ## Summary
    
    When Codex is launched from a subdirectory of a Git repository, the
    onboarding trust prompt says it is trusting the current directory even
    though the persisted trust target is the repository root. That can make
    the scope of the trust decision unclear.
    
    This updates the TUI trust prompt to show a yellow note only when the
    current directory differs from the resolved trust target, explaining
    that trust applies to the repository root and displaying that root. It
    also removes the stale onboarding TODO now that the warning is
    implemented.
  • Fix exec inheritance of root shared flags (#18630)
    Addresses #18113
    
    Problem: Shared flags provided before the exec subcommand were parsed by
    the root CLI but not inherited by the exec CLI, so exec sessions could
    run with stale or default sandbox and model configuration.
    
    Solution: Move shared TUI and exec flags into a common option block and
    merge root selections into exec before dispatch, while preserving exec's
    global subcommand flag behavior.
  • Refactor TUI app module into submodules (#18753)
    ## Why
    
    The TUI app module had grown past the 512K source-file cap enforced by
    CI/CD. This keeps the app entry point below that limit while preserving
    the existing runtime behavior and test surface.
    
    ## What changed
    
    - Kept the top-level `App` state and run-loop wiring in
    `tui/src/app.rs`.
    - Split app responsibilities into focused private submodules under
    `tui/src/app/`, covering event dispatch, thread routing, session
    lifecycle, config persistence, background requests, startup prompts,
    input, history UI, platform actions, and thread event buffering.
    - Moved the existing app-level tests into `tui/src/app/tests.rs` and
    reused the existing snapshot location rather than adding new tests or
    snapshots.
    - Added module header comments for `app.rs` and the new submodules.
    
    ## Follow-up
    
    A future cleanup can move narrow unit tests from `tui/src/app/tests.rs`
    into the specific app submodules they exercise. This PR keeps the
    existing app-level tests together so the refactor stays focused on the
    source-file split.
    
    ## Verification
    
    - `cargo test -p codex-tui --lib
    app::tests::agent_picker_item_name_snapshot`
    - `cargo test -p codex-tui --lib app::tests::clear_ui`
    - `cargo test -p codex-tui --lib
    app::tests::ctrl_l_clear_ui_after_long_transcript_reuses_clear_header_snapshot`
    - `just fix -p codex-tui`
    
    Full `cargo test -p codex-tui` still fails on model-catalog drift
    unrelated to this refactor, including stale
    `gpt-5.3-codex`/`gpt-5.1-codex` snapshot and migration expectations now
    resolving to `gpt-5.4`.
  • Add realtime silence tool (#18635)
    ## Summary
    
    Adds a second realtime v2 function tool, `remain_silent`, so the
    realtime model has an explicit non-speaking action when the
    collaboration mode or latest context says it should not answer aloud.
    This is stacked on #18597.
    
    ## Design
    
    - Advertise `remain_silent` alongside `background_agent` in realtime v2
    conversational sessions.
    - Parse `remain_silent` function calls into a typed
    `RealtimeEvent::NoopRequested` event.
    - Have core answer that function call with an empty
    `function_call_output` and deliberately avoid `response.create`, so no
    follow-up realtime response is requested.
    - Keep the event hidden from app-server/TUI surfaces; it is operational
    plumbing, not user-visible conversation content.
  • 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.
  • fix(tui): keep /copy aligned with rollback (#18739)
    ## Why
    
    Fixes #18718.
    
    After rewinding a thread, `/copy` could still copy the latest assistant
    response from before the rewind. The transcript cells were rolled back,
    but the copy source was a single `last_agent_markdown` cache that was
    not synchronized with backtracking, so the visible conversation and
    copied content could diverge.
    
    ## What changed
    
    `ChatWidget` now keeps a bounded copy history for the most recent 32
    assistant responses, keyed by the visible user-turn count. When local
    rollback trims transcript cells, the copy cache is trimmed to the same
    surviving user-turn count so `/copy` uses the latest visible assistant
    response.
    
    If the user rewinds past the retained copy window, `/copy` now reports:
    
    ```text
    Cannot copy that response after rewinding. Only the most recent 32 responses are available to /copy.
    ```
    
    The change also adds coverage for copying the latest surviving response
    after rollback and for the over-limit rewind message.
    
    ## Verification
    
    - Manually resumed a synthetic 35-turn session, rewound within the
    retained window, and verified `/copy` copied the surviving response.
    - Manually rewound past the retained window and verified `/copy` showed
    the 32-response limit message.
    - `cargo test -p codex-tui slash_copy`
    - `just fix -p codex-tui`
    - `cargo insta pending-snapshots`
    
    Note: `cargo test -p codex-tui` currently fails on unrelated model
    catalog and snapshot drift around the default model changing to
    `gpt-5.4`; the focused `/copy` tests pass after fixing the new test
    setup.
  • Fix stale model test fixtures (#18719)
    Fixes stale test fixtures left after the active bundled model catalog
    updates in #18586 and #18388. Those changes made `gpt-5.4` the current
    default and removed several older hardcoded slugs, which left Windows
    Bazel shards failing TUI and config tests.
    
    What changed:
    - Refresh TUI model migration, availability NUX, plan-mode, status, and
    snapshot fixtures to use active bundled model slugs.
    - Update the config edit test expectation for the TOML-quoted
    `"gpt-5.2"` migration key.
    - Move the model catalog tests into
    `codex-rs/tui/src/app/tests/model_catalog.rs` so touching them does not
    trip the blob-size policy for `app.rs`.
    
    Verification:
    - CI Bazel/lint checks are expected to cover the affected test shards.
  • Surface TUI skills refresh failures (#18627)
    ## Why
    
    `skills/list` refreshes are best-effort metadata updates. If one fails
    during startup or thread switching, the TUI should keep running and show
    enough detail to diagnose the app-server failure instead of leaving the
    user with only a log entry.
    
    This addresses the recoverability and observability issue reported in
    #16914.
    
    ## What Changed
    
    - Preserve the full startup `skills/list` error chain before sending it
    back through the app event queue.
    - Surface failed skills refreshes as recoverable TUI error messages
    while still logging the warning.
    
    This is related to the recent bug fix from [PR
    #18370](https://github.com/openai/codex/pull/18370).