7919 Commits

  • Add environmentId to request_permissions (#25858)
    ## Stack
    
    1. #25850 - Key request-permission grants by environment: stores and
    applies sticky permission grants per environment id.
    2. This PR (#25858) - Add `environmentId` to `request_permissions`: lets
    the model target a selected environment and resolves relative permission
    paths against it.
    3. #25862 - Propagate permission approval environment id: carries the
    selected environment id through approval events, app-server requests,
    TUI prompts, and delegate forwarding.
    4. #25867 - Add remote request permissions integration coverage:
    verifies the selected remote environment across request, approval, grant
    reuse, and exec.
    
    This PR is stacked on #25850; #25862 and #25867 are stacked on this PR.
    
    ## Why
    
    PR1 made request-permission grants internally environment-keyed, but the
    model-facing `request_permissions` tool could still only target the
    primary environment. For CCA and multi-environment turns, the tool needs
    an explicit way to bind a permission request to a selected attached
    environment before resolving relative paths.
    
    ## What Changed
    
    - Added optional `environmentId` to `RequestPermissionsArgs`, with
    `environment_id` accepted as an alias.
    - Exposed `environmentId` in the `request_permissions` tool schema and
    description.
    - Resolve the selected environment before parsing filesystem permission
    paths, so relative paths bind to the selected environment cwd.
    - Route validated tool calls through
    `request_permissions_for_environment` directly instead of duplicating
    environment lookup in `Session::request_permissions`.
    - Reject unknown environment ids with a model-facing error.
    - Updated focused request-permissions and Guardian call sites for the
    new optional field.
    
    ## Testing
    
    Not run locally per instruction.
  • [codex-analytics] Track CodexErr details in turn analytics (#25707)
    ## Summary
    - add analytics-only `CodexErr` telemetry to `codex_turn_event` while
    leaving existing `turn_error` unchanged
    - record terminal `CodexErr` facts from core immediately before the
    existing turn error event is sent
    - emit source-truth `codex_error_*` fields for downstream analytics,
    including the raw `CodexErr::InvalidRequest(String)` message as
    `codex_error_subreason`
    
    ## Validation
    - `just test -p codex-analytics`
    - attempted `just test -p codex-core`, but the local run timed out
    across unrelated integration suites in this environment and is not being
    used as validation
  • Key request-permission grants by environment (#25850)
    ## Stack
    
    1. This PR (#25850) - Key request-permission grants by environment:
    stores and applies sticky permission grants per environment id.
    2. #25858 - Add `environmentId` to `request_permissions`: lets the model
    target a selected environment and resolves relative permission paths
    against it.
    3. #25862 - Propagate permission approval environment id: carries the
    selected environment id through approval events, app-server requests,
    TUI prompts, and delegate forwarding.
    4. #25867 - Add remote request permissions integration coverage:
    verifies the selected remote environment across request, approval, grant
    reuse, and exec.
    
    #25858, #25862, and #25867 are stacked on this PR and should be reviewed
    after it.
    
    ## Why
    
    Multi-environment CCA turns can attach both local and remote executors,
    but request-permission grants were still effectively cwd-only. Pending
    permission requests tracked a cwd, while stored turn/session grants had
    no environment identity, so sticky grants could be reused through the
    wrong executor context.
    
    This makes the first permission-grant step environment-aware without
    changing the external `request_permissions` payload shape: omitted
    environment targeting remains bound to the primary turn environment.
    
    ## What Changed
    
    - Store turn- and session-scoped request-permission grants by
    `environment_id`.
    - Keep the selected `TurnEnvironmentSelection` with pending
    `request_permissions` calls so approval responses normalize and record
    grants against the same environment.
    - Resolve relative `request_permissions` file paths against the primary
    turn environment cwd instead of deprecated `turn.cwd`.
    - Apply sticky grants in `shell`, `exec_command`, and `apply_patch` by
    selected environment id while still using the actual tool cwd for
    cwd-relative permission materialization.
    - Update Guardian and request-permissions coverage for the
    environment-keyed grant behavior.
    
    ## Testing
    
    Not run locally. Added or updated focused coverage for:
    
    - `request_permission_grants_are_environment_keyed`
    -
    `request_permissions_tool_resolves_relative_paths_against_primary_environment`
    - related Guardian/request-permissions sticky grant tests
  • [codex] Revert shared BuildBuddy Bazel wrapper (#25909)
    ## Why
    
    PR #25905 intentionally adds a failing `codex-core` unit test, but its
    [Bazel test on Windows
    check](https://github.com/openai/codex/actions/runs/26837526950/job/79135369259)
    passed. That shows the Bazel configuration introduced by #25156 is not
    behaving as expected, so revert it while the configuration can be
    investigated separately.
    
    ## What changed
    
    Revert #25156 in full, restoring the previous Bazel remote
    configuration, CI scripts, workflows, `rusty_v8` handling, and
    documentation. This removes the shared BuildBuddy wrapper and its tests.
    
    ## Validation
    
    Not run locally; this exact revert was prioritized for a fast rollback.
  • core: derive built-in permission profiles from raw policies (#25739)
    ## Why
    
    Permission profiles that extend a built-in profile should behave like
    other TOML inheritance: parent entries provide defaults, and child keys
    override matching fields before the profile is compiled.
    
    That was not true for `:workspace`. Previously, a profile with `extends
    = ":workspace"` seeded the compiled runtime
    `PermissionProfile::workspace_write()` policy and then appended child
    filesystem entries. A child override such as `":tmpdir" = "read"`
    therefore left the inherited `":tmpdir" = "write"` entry in the final
    policy. Since same-target `write` wins over `read` during runtime
    resolution, the child override was ineffective.
    
    This also needs a clear source of truth for the built-in profiles. The
    protocol-level sandbox policy constructors now define the raw built-in
    filesystem entries, and both `PermissionProfile` presets and
    config-profile inheritance derive from those same values.
    
    ## What Changed
    
    - Add a canonical `FileSystemSandboxPolicy::read_only()` constructor
    while keeping the read-only and workspace-write raw filesystem entries
    explicit and independent.
    - Derive `PermissionProfile::read_only()` from
    `FileSystemSandboxPolicy::read_only()`;
    `PermissionProfile::workspace_write()` continues to derive from
    `FileSystemSandboxPolicy::workspace_write()`.
    - Build extensible `:read-only` and `:workspace` parent profiles by
    projecting those canonical sandbox policies into
    `PermissionProfileToml`, then merge user overrides at the TOML layer
    before compilation.
    - Add config parsing support for `:slash_tmp` so the built-in
    `:workspace` parent can be expressed in the same TOML-shaped filesystem
    table as user profiles.
    - Document that `PermissionsToml::resolve_profile()` returns an
    already-merged `PermissionProfileToml`, and return that profile directly
    after removing the resolved-profile wrapper.
    - Extend the config test for `extends = ":workspace"` to assert that
    inherited `":slash_tmp" = "write"` is preserved and that a child
    `":tmpdir" = "read"` entry replaces the inherited `write` entry.
    
    ## Verification
    
    - `just test -p codex-config`
    - `just test -p codex-protocol`
    - `just test -p codex-core
    permissions_profiles_resolve_extends_parent_first_with_child_overrides`
    - `just test -p codex-core
    default_permissions_profile_can_extend_builtin_workspace`
    - `just test -p codex-core`
      - Result: 2596 passed, 4 failed, 1 timed out.
    - The failures were existing sandbox/environment-sensitive tests
    unrelated to this permissions change:
    
    `suite::user_shell_cmd::user_shell_command_does_not_set_network_sandbox_env_var`,
    
    `suite::user_shell_cmd::user_shell_command_history_is_persisted_and_shared_with_model`,
    
    `suite::abort_tasks::interrupt_persists_turn_aborted_marker_in_next_request`,
        `suite::abort_tasks::interrupt_tool_records_history_entries`, and
    
    `thread_manager::tests::start_thread_uses_all_default_environments_from_codex_home`.
  • Route Bazel CI through shared BuildBuddy remote config wrapper (#25156)
    ## Why
    
    Bazel remote configuration was selected in several CI scripts and
    workflow steps. That made the BuildBuddy tenant policy easy to duplicate
    and harder to audit, especially for fork pull requests that must not use
    the OpenAI tenant.
    
    This builds on
    [sluongng/buildbuddy-ci-host-routing](https://github.com/openai/codex/compare/main...sluongng:codex:sluongng/buildbuddy-ci-host-routing)
    and consolidates the policy in one place.
    
    ## What to do if this breaks you
    
    See `codex-rs/docs/bazel.md` for details. TLDR:
    
    1. make a BuildBuddy API key and put it in `~/.bazelrc`
    2. if you're an OpenAI employee, add `common
    --config=buildbuddy-openai-rbe` to `user.bazelrc` in the repo root
    
    Run `just bazel-test` to ensure it works.
    
    Note that `just bazel-remote-test` no longer exists, you need to select
    a remote configuration as documented to use RBE.
    
    ## What changed
    
    - Add `.github/scripts/run_bazel_with_buildbuddy.py` as the shared Bazel
    wrapper and Python library. It selects the OpenAI host only for trusted
    upstream GitHub Actions runs, routes keyed fork runs to the generic
    host, and falls back to local Bazel execution when no key is available.
    - Move endpoint selection into explicit `.bazelrc` configurations and
    update Bazel CI, query helpers, and `rusty_v8` staging to use the shared
    policy. Loading-phase target-discovery queries remain local.
    - Add wrapper and `rusty_v8` unit coverage, plus `just test-scripts` for
    the `.github/scripts` Python tests.
    - Document local Bazel usage, `user.bazelrc` setup, BuildBuddy
    configurations, and CI behavior in `codex-rs/docs/bazel.md`.
    
    ## Validation
    
    - `just test-scripts`
    - `bash -n .github/scripts/run-bazel-ci.sh
    .github/scripts/run-bazel-query-ci.sh
    .github/scripts/run-argument-comment-lint-bazel.sh
    scripts/list-bazel-clippy-targets.sh`
    - `python3 -m py_compile .github/scripts/run_bazel_with_buildbuddy.py
    .github/scripts/test_run_bazel_with_buildbuddy.py
    .github/scripts/test_rusty_v8_bazel.py
    .github/scripts/rusty_v8_bazel.py`
    - `ruff check .github/scripts/run_bazel_with_buildbuddy.py
    .github/scripts/test_run_bazel_with_buildbuddy.py
    .github/scripts/test_rusty_v8_bazel.py
    .github/scripts/rusty_v8_bazel.py`
  • Skip startup prewarm when websockets are disabled (#25868)
    ## Summary
    - skip startup websocket prewarm setup when the model client has
    Responses-over-WebSocket disabled
    - avoid making HTTP-only sessions build prewarm prompt/tool state that
    cannot produce a reusable websocket session
    
    ## Why
    Recent macOS timing flakes were timing out while waiting for first-turn
    events in HTTP-only core tests. Startup prewarm is only useful for
    websocket-capable providers, but it was scheduled for every session. For
    HTTP-only test providers this added unnecessary async startup work
    before the regular turn could reach the mocked response flow.
    
    ## Testing
    - bazel test //codex-rs/core:core-all-test
    --test_filter=suite::auto_review::remote_model_override_uses_catalog_model_for_strict_auto_review
    --test_output=errors
    - bazel test //codex-rs/core:core-all-test
    --test_filter=suite::request_permissions_tool::approved_folder_write_request_permissions_unblocks_later_apply_patch
    --test_output=errors
  • [app-server][core] Add connector-level Guardian reviewer overrides (#25167)
    Context: https://openai.slack.com/archives/C0B4JAF0Q2C/p1779912328647229
    
    ```
    approvals_reviewer = "auto_review"
    
    [apps.connector_5f3c8c41a1e54ad7a76272c89e2554fa]
    enabled = true
    approvals_reviewer = "user"
    default_tools_approval_mode = "prompt"
    ```
    
    <img width="230" height="84" alt="Screenshot 2026-05-31 at 11 56 34 AM"
    src="https://github.com/user-attachments/assets/e319f8f7-0983-42a7-98cd-3302732fa406"
    />
    
    <img width="841" height="233" alt="Screenshot 2026-05-31 at 11 52 42 AM"
    src="https://github.com/user-attachments/assets/7ac76645-4e90-4d00-8242-f031146a22a5"
    />
    
    -------
    
    ```
    approvals_reviewer = "user"
    
    [apps.connector_5f3c8c41a1e54ad7a76272c89e2554fa]
    enabled = true
    approvals_reviewer = "auto_review"
    default_tools_approval_mode = "prompt"
    ```
    <img width="195" height="83" alt="Screenshot 2026-05-31 at 12 02 27 PM"
    src="https://github.com/user-attachments/assets/3d374dc8-8aa2-466f-a13f-e4ed8567aa2e"
    />
    <img width="771" height="207" alt="Screenshot 2026-05-31 at 12 05 42 PM"
    src="https://github.com/user-attachments/assets/105c2575-68d6-4ca6-8e69-dc8c82da36a2"
    />
    
    
    
    ## Summary
    - add `apps.<connector_id>.approvals_reviewer` to override Guardian or
    user review routing per connected app
    - apply overrides across direct app MCP calls, delegated MCP prompts,
    and app-server MCP elicitation review while preserving global behavior
    for non-app MCP servers
    - expose and document the config through app-server v2 and generated
    schemas, while honoring global managed reviewer requirements
    
    ---------
    
    Co-authored-by: jif-oai <jif@openai.com>
  • [codex] Use git CLI for Cargo fetches across Rust workflows (#25775)
    ## Why
    Cargo's libgit2 transport has intermittently failed while fetching git
    dependencies with nested submodules.
    [#25644](https://github.com/openai/codex/pull/25644) applied
    `CARGO_NET_GIT_FETCH_WITH_CLI=true` to the main Rust release build after
    macOS SecureTransport/libgit2 failures while cloning `libwebrtc`'s
    nested `libyuv` submodule. Similar flakes can affect other Cargo-bearing
    Rust jobs.
    
    ## What changed
    Configure `CARGO_NET_GIT_FETCH_WITH_CLI=true` at workflow scope for the
    remaining Cargo-bearing Rust workflows:
    
    - fast Rust CI and `cargo-deny`
    - reusable Windows and argument-comment-lint release workflows
    - `rusty-v8-release` and `v8-canary` Cargo builds and smoke tests
    
    The full Rust CI, reusable nextest workflow, and primary Rust release
    build already had the override. Bazel-only workflows are unchanged
    because they use a different dependency fetch path.
    
    ## Validation
    - Parsed all `.github/workflows/*.yml` files as YAML.
    - Scanned Cargo-bearing workflows to confirm they configure
    `CARGO_NET_GIT_FETCH_WITH_CLI`.
  • Run Codex async main on a sized stack (#25847)
    ## Why
    
    `Runtime::block_on` executes the top-level future on the caller's OS
    thread, not on one of Tokio's worker threads. That matters for the
    interactive CLI because the Tokio runtime already configures larger
    worker stacks, while the process main thread can still have a smaller
    platform default stack.
    
    This showed up as a `/clear` crash on macOS: starting a fresh TUI thread
    reloads config, and the stack-heavy TOML deserialization path can
    overflow before the new session is actually started.
    
    ## What Changed
    
    - Run the regular `arg0_dispatch_or_else` async entrypoint on a named
    `codex-main` thread.
    - Give that thread the same `TOKIO_WORKER_STACK_SIZE_BYTES` stack budget
    already used for Tokio worker threads.
    - Keep `Arg0DispatchPaths` and the arg0 alias guard lifetime behavior
    the same.
    - Resume panics from the spawned main thread so panic behavior is
    preserved.
    
    ## Verification
    
    - `cargo check -p codex-cli` currently fails because the top-level
    CLI/TUI future is not `Send` under the new thread boundary.
  • flake: Keep plugin test homes alive (#25857)
    ## Summary
    
    Keep the full `TestCodex` harness alive in plugin integration tests
    instead of returning only the `CodexThread`.
    
    ## Why
    
    The helper was moving a temporary `codex_home` into `TestCodex`, then
    immediately dropping the harness and returning only the thread. For
    plugin MCP tests, the MCP server cwd is inside that temporary home. If
    the temp directory is removed while MCP startup is still racing, the
    server launch can fail with `No such file or directory`.
    
    Keeping the harness in scope keeps the temp home alive for the test
    duration and removes the lifetime race behind the recent
    `explicit_plugin_mentions_inject_plugin_guidance` flake.
    
    ## Validation
    
    - `just fmt`
    - `just test -p codex-core
    explicit_plugin_mentions_inject_plugin_guidance`
  • Reduce stack pressure in session startup and config rebuilds (#25844)
    ## Why
    
    `/clear` starts a fresh thread with `InitialHistory::Cleared`, which
    re-enters the thread/session startup path. That path now builds large
    async futures through `ThreadManagerState::spawn_thread_with_source`,
    `Codex::spawn`, and `Session::new`. Separately, TUI config rebuilds for
    cwd and permission-profile changes build a similarly heavy
    `ConfigBuilder::build()` future inside the app task. In debug and Bazel
    runs, those call chains can put enough state on the caller stack to
    abort before startup or config refresh completes.
    
    This change keeps the behavior the same while moving the heaviest future
    frames off the caller stack.
    
    ## What changed
    
    - Box `Codex::spawn(...)` in `codex-rs/core/src/thread_manager.rs`
    before awaiting it from `spawn_thread_with_source`.
    - Box `Session::new(...)` in `codex-rs/core/src/session/mod.rs` before
    awaiting it from `Codex::spawn_internal`.
    - Route `ConfigBuilder::build()` through a small `tokio::spawn` helper
    in `codex-rs/tui/src/app/config_persistence.rs` so cwd and
    permission-profile config rebuilds run on a runtime worker stack while
    preserving error context.
    
    ## Verification
    
    CI is running on the PR.
    
    No new targeted tests were added. This is a mechanical stack-pressure
    reduction that keeps the existing behavior and error propagation intact.
  • Test runtime selector before first turn (#25724)
    Stack split from #25708. Original PR intentionally left open. This fifth
    PR adds coverage that a remotely selected multi-agent runtime is applied
    when the model is selected before the first turn.
  • Test remote multi-agent runtime selector override (#25723)
    Stack split from #25708. Original PR intentionally left open. This
    fourth PR adds coverage that remote model multi-agent runtime selectors
    override local feature flag defaults.
  • fix: main oops (#25840)
    Fix main, comment is self-explainatory
  • session: keep startup prewarm aligned with resolved multi-agent runtime (#25841)
    ## Why
    
    Follow-up to #25722. Startup prewarm builds a preview `TurnContext`
    before the first real turn so it can precompute the initial prompt and
    tool surface. After the per-thread runtime work landed, that preview
    path still recomputed multi-agent mode from `model_info` and feature
    defaults instead of reusing the runtime the session had already resolved
    from persisted metadata or inheritance.
    
    That could leave the prewarmed session primed for a different
    multi-agent mode than the first real turn, which is especially risky
    because collaboration tool exposure depends on
    `turn_context.multi_agent_version`.
    
    ## What changed
    
    - In the `TurnMultiAgentRuntime::Preview` path, prefer
    `Session::multi_agent_version()` when it is already known.
    - Only fall back to `model_info.multi_agent_version` and feature
    defaults when the session has not resolved a runtime yet.
    - Keep preview mode read-only: this still avoids storing a runtime
    during startup prewarm.
    
    ## Testing
    
    - Not run (small runtime-selection follow-up)
  • Resolve per-thread multi-agent runtime (#25722)
    Stack split from #25708. Original PR intentionally left open. This third
    PR resolves the effective per-thread multi-agent runtime from persisted
    metadata, inherited runtime, and current model selection.
  • Persist multi-agent runtime metadata (#25721)
    Stack split from #25708. Original PR intentionally left open. This
    second PR persists multi-agent runtime metadata through thread creation,
    rollout recording, and thread storage.
  • Add multi-agent runtime metadata types (#25720)
    Stack split from #25708. Original PR intentionally left open. This first
    PR adds the multi-agent runtime metadata types and catalog plumbing used
    by the rest of the stack.
  • feat: reuse compressed rollout search snippets (#25814)
    ## Summary
    - teach rollout search to return precomputed snippets for compressed
    rollouts
    - reuse those snippets in local thread search instead of reopening
    matching compressed files
    - keep the no-`rg` fallback single-pass and add regression coverage for
    the compressed path
    
    ## Why
    `thread/search` currently decodes matching compressed rollouts twice:
    once to discover the matching path and again to extract the snippet
    shown in results. That defeats a meaningful part of the compressed-read
    optimization work.
    
    ## Impact
    Compressed rollout hits now pay one decode pass on the search path while
    plain `.jsonl` hits keep the existing ripgrep-driven flow.
    
    ## Validation
    - `just test -p codex-rollout`
    - `just test -p codex-thread-store`
    - `just fix -p codex-rollout`
    - `just fix -p codex-thread-store`
    - `just fmt`
  • [codex] Validate plugin skill base names (#25782)
    ## Summary
    
    - Validate skill base name length before plugin namespacing.
    - Bound the composed `plugin:skill` qualified name to 128 characters.
    - Keep plugin skill runtime names in the existing `plugin:skill` form.
    - Add regression tests for the max qualified-name boundary and rejection
    path.
    
    ## Root Cause
    
    Plugin skills are represented as `plugin_name:skill_name`, but the
    loader previously applied the 64-character skill name limit after adding
    the plugin namespace. Moving that check to the base name fixes valid
    plugin skills with longer namespaces, and the separate 128-character
    qualified-name limit keeps model-visible skill names bounded.
    
    ## Validation
    
    - `just fmt`
    - `just test -p codex-core-skills plugin_skill_name_length_limit`
    - `git diff --check`
  • [codex] Move plugin discoverable logic into core-plugins (#25783)
    ## Summary
    - Move plugin discoverable recommendation filtering from `codex-core`
    into `codex-core-plugins` behind `ToolSuggestPluginDiscoveryInput`.
    - Keep `codex-core` as a thin adapter from `Config` to the core-plugins
    API and back to `DiscoverablePluginInfo`.
    - Keep the existing discoverable allowlist private to the core-plugins
    implementation.
    
    ## Validation
    - `just fmt`
    - `just test -p codex-core list_tool_suggest_discoverable_plugins`
    - `git diff --check`
    - Read-only subagent review: no findings
  • [codex] Cache remote plugin catalog for suggestions (#25457)
    ## Summary
    - cache the global remote plugin catalog when remote plugin listing runs
    and warm it during startup
    - use the cached remote catalog in plugin install recommendations with
    canonical `plugin@openai-curated-remote` ids
    - reuse the session `PluginsManager` for plugin recommendations so
    remote cache state is visible on the recommend path
    - skip core installed-state verification for remote plugin install
    suggestions while leaving local plugin and connector verification
    unchanged
    
    ## Testing
    - `just fmt`
    - `git diff --check`
    - `cargo test -p codex-core
    list_tool_suggest_discoverable_plugins_includes_cached_remote_global_plugins`
    - `cargo test -p codex-core
    remote_plugin_install_suggestions_skip_core_installed_verification`
    - `cargo test -p codex-app-server
    plugin_list_includes_remote_marketplaces_when_remote_plugin_enabled`
    
    Earlier focused checks during the same branch: codex-tools TUI filter
    test, request_plugin_install tests, and codex-app-server build.
  • [codex] Add plugin list JSON output (#25330)
    ## Summary
    - add `--json` output to `codex plugin list` with `installed` and
    `available` arrays
    - add `--available` for JSON output only; using it without `--json` is
    rejected
    - keep the existing non-JSON table output unchanged
    - add CLI coverage for JSON installed/available output and the
    `--available`/`--json` requirement
    
    ## Validation
    - `just test -p codex-cli plugin_list`
    - `just fix -p codex-cli`
    - `git diff --check`
    
    Note: `just fmt` ran Rust formatting first, then failed in the Python
    ruff step because `openai-codex-cli-bin==0.132.0` has no wheel for this
    Linux platform.
  • feat: show enterprise monthly credit limits in status (#24812)
    ## Summary
    
    Enterprise users can have an effective monthly credit limit, but Codex
    `/status` currently drops that metadata from the account-usage response.
    
    This change adds the optional `spend_control.individual_limit`
    projection to the existing rate-limit snapshot flow. The backend client
    reads the monthly limit, app-server exposes it as `individualLimit`, and
    the TUI renders a `Monthly credit limit` row through the existing
    progress-bar renderer.
    
    When the backend does not return an effective monthly limit, existing
    rate-limit behavior is unchanged.
    
    ## Existing backend state
    
    The account-usage backend already returns the effective monthly limit
    and current usage together:
    
    ```json
    {
      "spend_control": {
        "reached": false,
        "individual_limit": {
          "limit": "25000",
          "used": "8000",
          "remaining": "17000",
          "used_percent": 32,
          "remaining_percent": 68,
          "reset_after_seconds": 86400,
          "reset_at": 1778137680
        }
      }
    }
    ```
    
    Before this change, Codex projected rolling `primary` and `secondary`
    windows plus `credits`. It ignored `spend_control.individual_limit`, so
    app-server clients and `/status` could not render the monthly cap.
    
    The updated flow is:
    
    ```text
    account usage backend
      -> backend-client reads spend_control.individual_limit
      -> existing rate-limit snapshot carries optional individual_limit
      -> app-server exposes optional individualLimit
      -> TUI renders Monthly credit limit
    ```
    
    ## App-server contract
    
    `account/rateLimits/read` and sparse `account/rateLimits/updated`
    notifications now include an additive nullable
    `rateLimits.individualLimit` field:
    
    ```json
    {
      "individualLimit": {
        "limit": "25000",
        "used": "8000",
        "remainingPercent": 68,
        "resetsAt": 1778137680
      }
    }
    ```
    
    In an `account/rateLimits/read` response, `null` means no monthly limit
    is available. `account/rateLimits/updated` remains a sparse rolling
    notification: clients merge available values into their most recent
    `account/rateLimits/read` snapshot or refetch. Nullable account metadata
    in a rolling notification does not clear a previously observed value.
    
    ## Design decisions
    
    - Extend the existing rate-limit snapshot instead of introducing a
    separate request or wire-level update protocol.
    - Keep the Codex projection narrow: `/status` needs the effective limit,
    current usage, remaining percentage, and reset timestamp.
    - Render the monthly row through the existing progress-bar renderer,
    with one optional detail line for `8,000 of 25,000 credits used`.
    - Keep the backend response optional so existing accounts and older
    usage states preserve their current behavior.
    - Preserve cached monthly metadata when sparse rolling notifications
    omit it. Live account-usage reads remain authoritative and can clear a
    removed limit.
    
    ## Visual evidence
    
    ```text
     Monthly credit limit:   [██████████████░░░░░░] 68% left (resets 07:08 on 7 May)
                             8,000 of 25,000 credits used
    ```
    
    Snapshot:
    `codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_enterprise_monthly_credit_limit.snap`
    
    ## Testing
    
    Tests: generated app-server schema verification, protocol tests,
    backend-client tests, app-server integration coverage, TUI snapshot
    coverage, formatting, and workspace lint cleanup.
  • Move code review rules into AGENTS (#25738)
    ## Why
    Codex Review now supports repository-specific review rules in AGENTS.md.
    Adding the review prompts there makes the guidance available as
    repository review rules next to the code it governs while keeping the
    existing local review skills intact.
    
    ## What changed
    - Added a `## Code Review Rules` section to `AGENTS.md` with the
    existing review prompts for model context, breaking changes, test
    authoring, and change size.
    - Preserved the existing `.codex/skills/code-review*` skill files.
    
    ## Verification
    - `git diff --check origin/main...HEAD`
  • [codex] Add comprehensive root formatting check (#25683)
    ## Why
    
    The root formatting entrypoints could drift: `just fmt` did not format
    the Justfile itself, and the CI-facing check recipe only checked Python
    scripts instead of matching everything formatted by `just fmt`.
    
    ## What changed
    
    - Add a shared cross-platform Python formatter driver used by both `just
    fmt` and `just fmt-check`.
    - Run Justfile, Rust, Python SDK, and internal-script formatter groups
    concurrently while buffering each formatter group's output until it
    finishes.
    - Log formatter starts immediately, then print each formatter group's
    labeled output when it completes.
    - Keep the SDK lint-fix and Ruff formatting passes ordered, with source
    comments explaining their distinct roles and the check-mode equivalents.
    - Run Ruff through shared `uv run --no-sync --with ruff` overlays so
    formatting works on clean glibc Linux checkouts without installing the
    platform-specific SDK runtime wheel.
    - Show `fmt-check` help text in `just -l` and simplify CI to call the
    shared driver through `just fmt-check`.
    - Pin the general CI workflow to `just@1.51.0` so its formatter agrees
    with the checked-in Justfile.
    - Add regression coverage for the thin Just recipes and the driver's
    formatter graph.
    
    ## Validation
    
    - `just fmt`
    - `just fmt-check`
    - `python3 -m pytest
    sdk/python/tests/test_artifact_workflow_and_binaries.py -k 'root_fmt or
    root_format' -q`
    - `pnpm run format`
    - `git diff --check`
    - `just -l | rg -n '^    fmt|fmt-check'`
    - `uvx --from uv==0.7.22 uv run --frozen --project sdk/python --no-sync
    --with ruff ruff check --diff sdk/python`
  • feat(remote-control): add pairing start (#25675)
    ## Why
    
    Remote control enrollment authorizes a desktop server, but app-server v2
    did not expose the follow-up pairing operation needed to mint a
    short-lived controller pairing artifact from that enrolled server.
    Clients need a narrow RPC that starts pairing without exposing the
    backend `serverId` or conflating pairing with websocket connection
    state.
    
    Issue: N/A; internal remote-control pairing API change.
    
    ## What Changed
    
    Added experimental app-server v2 `remoteControl/pairing/start` with
    `manualCode` input and `pairingCode`, nullable `manualPairingCode`,
    `environmentId`, and Unix-seconds `expiresAt` output. The method
    serializes under its own `global("remote-control-pairing")` scope and is
    documented in `app-server/README.md`.
    
    Extended the remote-control transport with private `/server/pair`
    request/response types and normalized `pair_url` handling. Pairing uses
    the current enrolled server bearer, refreshes that bearer when needed,
    keeps backend `server_id` private, validates returned `server_id` and
    `environment_id` against the current enrollment, and preserves backend
    status/header/body context for failures and malformed responses.
    
    Wired the request through `RemoteControlRequestProcessor` and
    `MessageProcessor`, mapping unavailable/disabled pairing to
    `invalid_request` and backend failures to internal errors.
    
    ## Verification
    
    - `just test -p codex-app-server-transport`
    - `just test -p codex-app-server
    remote_control_pairing_start_returns_pairing_artifacts`
  • Handle invalid plugin skills manifest field (#25717)
    ## Summary
    - Treat invalid `plugin.json` `skills` shapes as a field-level warning
    instead of rejecting the whole manifest
    - Keep valid string path behavior unchanged and continue falling back to
    the default `skills/` root
    - Add regression coverage for array-shaped `skills`
    
    ## Tests
    - `just fmt`
    - `cargo test -p codex-core-plugins`
  • Move cloud requirements crate to cloud config (#24621)
    ## Summary
    
    - Moves the existing `codex-cloud-requirements` crate to
    `codex-cloud-config`.
    - Updates workspace dependencies and imports to the new crate name.
    - Intentionally keeps runtime behavior unchanged: this still fetches the
    legacy cloud requirements endpoint.
    
    ## Details
    
    This PR exists to make the lineage obvious before the bundle migration.
    GitHub should show the old `codex-rs/cloud-requirements/src/lib.rs`
    implementation as moved to `codex-rs/cloud-config/src/lib.rs`, rather
    than as unrelated new code.
    
    The follow-up PR adapts this moved crate to the new config bundle API
    and switches runtime consumers over.
  • app-server: remove experimental persist_extended_history bool flag (#25712)
    ## Summary
    
    Remove the dead experimental `persistExtendedHistory` app-server flag
    and collapse rollout persistence to the single policy app-server already
    used.
    
    ## What Changed
    
    - Removed `persistExtendedHistory` from v2 thread start/resume/fork
    params and deleted its deprecation notice path.
    - Removed the persistence-mode enums and plumbing through core, rollout,
    and thread-store.
    - Made rollout filtering mode-free, keeping the existing limited
    persisted-history behavior.
    
    ## Test Plan
    
    - `just write-app-server-schema`
    - `cargo nextest run --no-fail-fast -p codex-app-server-protocol
    schema_fixtures`
    - `cargo nextest run --no-fail-fast -p codex-app-server
    thread_shell_command_history_responses_exclude_persisted_command_executions`
    - `cargo nextest run --no-fail-fast -p codex-rollout -p
    codex-thread-store`
    - final `rg` for removed flag/type names
  • Wire managed MITM CA trust into child env (#22668)
    ## Stack
    1. Parent PR: #18240 uses named MITM permissions config.
    2. This PR wires managed MITM CA trust into spawned child processes.
    
    ## Why
    When Codex terminates HTTPS for limited mode or MITM hooks, child HTTPS
    clients need to trust Codex's managed MITM CA. Exporting proxy URLs
    alone is not enough, but blindly replacing user CA settings would be
    wrong: it can break custom enterprise/test roots, leak unreadable CA
    files into generated bundles, or make the child env disagree with its
    sandbox policy.
    
    ## Summary
    1. Build immutable managed CA bundles under `$CODEX_HOME/proxy` that
    include native roots, the managed MITM CA, and only inherited or
    command-scoped CA bundles the child is allowed to read.
    2. Export curated CA env vars alongside managed proxy env vars while
    preserving user CA override semantics, including nested Codex
    `SSL_CERT_FILE` precedence.
    3. Thread generated CA bundle paths into child sandbox readable roots,
    including debug sandbox execution, so the exported env vars work inside
    sandboxed commands.
    4. Remove only Codex-generated MITM CA bundle env when a child
    intentionally drops managed proxying for escalation or no-proxy retry.
    5. Document the managed CA bundle behavior and cover env injection,
    per-child bundle generation, sandbox readable roots, and no-proxy
    cleanup in tests.
    
    ## Validation
    1. Ran `just test -p codex-network-proxy`.
    2. Ran `just test -p codex-protocol`.
    3. Ran `just fix -p codex-network-proxy -p codex-protocol`.
    4. Tried focused `codex-core` validation, but the crate currently fails
    to compile in `core/tests/suite/guardian_review.rs` because an existing
    `Op::UserInput` initializer is missing `additional_context`.
    
    ---------
    
    Co-authored-by: Eva Wong <evawong@openai.com>
  • Reject directory rollout paths for pathless side chats (#25661)
    ## Why
    
    Fixes openai/codex#20944.
    
    Desktop side chats are intentionally ephemeral and pathless. They can
    still accept live turns while loaded, but after a reload there is no
    persisted rollout to resume. In the reported failure mode, Desktop could
    send `$CODEX_HOME` as the resume/fork path for one of these pathless
    side chats.
    
    `thread/resume` and `thread/fork` prefer an explicit `path` over
    `threadId`, and rollout path lookup only checked that a candidate
    existed. That let `$CODEX_HOME` pass as a rollout path, so the later
    rollout reader tried to open a directory and surfaced the low-level `Is
    a directory` error.
    
    ## What Changed
    
    - Reject explicit rollout paths that resolve to a directory or other
    non-file before attempting to read rollout history.
    - Make `codex_rollout::existing_rollout_path` return only plain or
    compressed rollout candidates that are actual files.
    - Add an app-server regression test that creates an ephemeral fork, runs
    a turn while the side thread is loaded, simulates reload, then verifies
    both `thread/resume` and `thread/fork` reject `$CODEX_HOME` with `path
    is a directory` instead of the OS-level directory-read error.
    - Rebase over the `TestAppServer` rename and update the remaining stale
    test harness call sites to use `TestAppServer` with `app_server` local
    variables.
    
    Relevant code:
    
    - `thread-store/src/local/read_thread.rs` validates explicit rollout
    paths before rollout reading:
    https://github.com/openai/codex/blob/25b47c8f425d351aaba4baa955a8092064a1707b/codex-rs/thread-store/src/local/read_thread.rs#L146-L165
    - `rollout/src/compression.rs` now requires file metadata for plain and
    compressed rollout candidates:
    https://github.com/openai/codex/blob/25b47c8f425d351aaba4baa955a8092064a1707b/codex-rs/rollout/src/compression.rs#L940-L950
    - The repro test covers the pathless ephemeral side-chat reload case:
    https://github.com/openai/codex/blob/25b47c8f425d351aaba4baa955a8092064a1707b/codex-rs/app-server/tests/suite/v2/thread_fork.rs#L774-L886
    
    ## Verification
    
    - `just test -p codex-app-server
    pathless_ephemeral_thread_rejects_codex_home_path_after_reload`
  • [codex] Publish release symbol artifacts (#25649)
    ## Why
    
    Production Codex binaries are stripped for distribution, which leaves
    crashes and samples from released builds without the symbols needed for
    useful stack traces. Publish symbols as separate release assets so
    production artifacts stay small while released builds remain
    symbolicateable.
    
    ## What changed
    
    - Add `.github/scripts/archive-release-symbols-and-strip-binaries.sh` to
    package platform-native symbols into `codex-symbols-<artifact>.tar.gz`
    assets while stripping the corresponding Unix binaries before signing.
    - Build release binaries with full debug information before producing
    distribution artifacts.
    - Publish macOS `.dSYM` bundles, Linux `.debug` files with
    `.gnu_debuglink`, and Windows `.pdb` files.
    - Strip Linux `bwrap` before computing its packaged-resource digest, but
    intentionally omit `bwrap` from symbol archives.
    - Preserve symbols artifacts in the unsigned macOS promotion flow.
    
    ## Verification
    
    - Ran `shellcheck` and `bash -n` on
    `.github/scripts/archive-release-symbols-and-strip-binaries.sh`.
    - Parsed the modified workflow YAML files and ran `git diff --check`.
    - Built a macOS release smoke binary and verified that the archived
    `.dSYM` contains DWARF application source information and has the same
    UUID as the stripped production binary.
    - Built Linux smoke binaries and verified that the symbol archive
    contains `codex.debug`, excludes `bwrap.debug`, leaves the expected
    `.gnu_debuglink` in `codex`, and does not mutate the separately stripped
    `bwrap` digest.
    - Staged a Windows smoke archive and verified that it contains the
    expected `.pdb` file.
  • fix(tui): clarify footer shortcut overlay hints (#25625)
    ## Why
    
    The TUI shortcut overlay used static labels for `Tab` and `Ctrl+C`, even
    though both keys change behavior while a task is running. That made the
    visible help misleading: idle `Tab` submits rather than queues, and
    active-turn `Ctrl+C` interrupts rather than exits.
    
    Closes #25531.
    Closes #25564.
    
    ## What Changed
    
    - Pass task-running state into the shortcut overlay renderer.
    - Render `Tab` as `submit message` while idle and `queue message` while
    work is running.
    - Render `Ctrl+C` as `exit` while idle and `interrupt` while work is
    running.
    - Add snapshot coverage for the active-work shortcut overlay and update
    idle overlay snapshots.
    
    ## How to Test
    
    1. Start Codex and open the shortcut overlay with `?` while no task is
    running.
    2. Confirm the overlay shows `tab to submit message` and `ctrl + c to
    exit`.
    3. Start a task, then open or keep the shortcut overlay visible while
    work is running.
    4. Confirm the overlay shows `tab to queue message` and `ctrl + c to
    interrupt`.
    5. Type a follow-up prompt during active work and press `Tab`; confirm
    it queues rather than submitting immediately.
    
    Targeted tests:
    
    - `just test -p codex-tui footer_snapshots`
    - `just test -p codex-tui footer_mode_snapshots`
    
    ## Validation Notes
    
    `just test -p codex-tui` currently has two unrelated guardian
    feature-flag test failures on this base:
    
    -
    `app::tests::update_feature_flags_disabling_guardian_clears_manual_review_policy_without_history`
    -
    `app::tests::update_feature_flags_disabling_guardian_clears_review_policy_and_restores_default`
    
    `just argument-comment-lint codex-rs/tui/src/bottom_pane/footer.rs`
    could not run locally because the prebuilt wrapper requires `dotslash`;
    the touched Rust diff was manually inspected for opaque positional
    literals.
  • Move tool search metadata onto ToolExecutor (#25684)
    Deferred tools need to be searchable even when they are not implemented
    inside `codex-core`. Extension-provided tools can be registered for
    later discovery, but the search metadata path was still owned by
    core-specific runtime hooks, which meant the shared `ToolExecutor`
    abstraction could not describe how a deferred extension tool should
    appear in `tool_search`.
    
    ## Changes
    
    - Move `ToolSearchEntry` and `ToolSearchInfo` into `codex-tools` and
    re-export them from the shared tools crate.
    - Add a default `ToolExecutor::search_info` implementation that derives
    loadable tool-search metadata from function and namespace specs.
    - Forward search metadata through extension adapters and exposure
    overrides while keeping custom search text/source metadata for dynamic,
    MCP, and multi-agent tools.
    - Remove the old core-local `tool_search_entry` module now that search
    metadata lives with the shared executor APIs.
    
    ## Testing
    
    - Added `deferred_extension_tools_are_discoverable_with_tool_search`
    coverage in `core/src/tools/spec_plan_tests.rs`.
  • Fix stale TestAppServer rename in plugin_list test (#25705)
    ## Why
    
    #25701 renamed the app-server test harness to `TestAppServer`, but it
    raced with #25681, which added a new `plugin_list` test call site still
    using the old `McpProcess` name. Once both changes met on `main`,
    app-server test builds failed before running the suite because
    `McpProcess` no longer exists in that scope.
    
    This PR fixes that CI break by updating the remaining stale call site to
    the renamed helper.
    
    ## What Changed
    
    - Replaced the `McpProcess::new(...)` use in
    `codex-rs/app-server/tests/suite/v2/plugin_list.rs` with
    `TestAppServer::new(...)`.
    - Renamed the local variable from `mcp` to `app_server` at the same call
    site to match the helper rename.
    
    Relevant code:
    https://github.com/openai/codex/blob/aadd9c999b4e0789f7afb2b9b8cc43000bb47e86/codex-rs/app-server/tests/suite/v2/plugin_list.rs#L234-L246
    
    ## Verification
    
    Not run locally; this is a compile fix for the app-server test harness
    rename.
  • [codex] enable parallel standalone web search calls (#25702)
    ## Summary
    - opt the extension-backed standalone `web.run` tool into parallel tool
    execution
    - update the existing extension registration test to assert that the
    tool advertises parallel-call support
    
    ## Why
    The standalone web-search API endpoint now supports parallel requests.
    The extension executor still inherited the shared serial default,
    causing multiple `web.run` calls to acquire the exclusive runtime lock.
    
    ## Impact
    Models that emit multiple standalone web-search calls can now execute
    them concurrently when model-level parallel tool calls are enabled.
    
    ## Validation
    - `just fmt`
    - `just test -p codex-web-search-extension`
    - `git diff --check origin/main...HEAD`
  • fix: rename McpServer to TestAppServer (#25701)
    This PR brought to you via VS Code rather than Codex...
    
    - opened `codex-rs/app-server/tests/common/mcp_process.rs`
    - put the cursor on `McpServer`
    - hit `F2` and renamed the symbol to `TestAppServer`
    - went to the file tree
    - hit enter and renamed `mcp_process.rs` to `test_app_server.rs`
    - ran **Save All Files** from the Command Palette
    - ran `just fmt`
    
    The End
    
    (Admittedly, most of the local variables for `TestAppServer` are still
    named `mcp`, though.)
  • fix: Deduplicate installed local and remote curated plugins (#25681)
    ## Summary
    - Deduplicate installed `openai-curated` and `openai-curated-remote`
    plugin conflicts by feature flag.
    - Prefer remote when remote plugins are enabled; otherwise prefer local,
    while preserving one-sided installs.
    
    ## Testing
    - `just fmt`
    - `git diff --check`
    - Targeted `just test` was blocked locally because `cargo-nextest` is
    not installed.
  • Add Python version compatibility guidance (#25690)
    ## Why
    
    Python contributions in this repository should target the declared
    Python 3 runtime instead of carrying Python 2 compatibility patterns
    forward. When compatibility across Python 3 point releases matters,
    contributors need a consistent source of truth for the minimum supported
    version.
    
    ## What changed
    
    - Added Python development guidance to `AGENTS.md` stating that the
    repository uses Python 3+ and should not use the `__future__` module.
    - Documented that contributors should check the nearest `pyproject.toml`
    `requires-python` field when evaluating Python 3 point-release
    compatibility.
    
    ## Testing
    
    Not run (guidance-only change).
  • [codex] Generalize deferred nested tool guidance (#25689)
    ## Summary
    - describe omitted code-mode tools as deferred nested tools instead of
    MCP/app tools
    - update the prompt-description assertion to match
    
    ## Why
    Deferred dynamic tools are also callable through `tools` and
    discoverable in `ALL_TOOLS`, so the previous MCP/app-specific wording
    was too narrow.
    
    ## Validation
    - `just fmt`
    - `just test -p codex-code-mode`
    - `git diff --check`
  • Add rollout compression histograms (#25680)
    ## Summary
    
    Stacked on #25679. Add histogram telemetry for rollout compression
    runtime, per-file compression time, byte sizes, and compression ratio.
    
    ## Changes
    
    - Emit `codex.rollout_compression.run.duration_ms` tagged by final run
    status.
    - Emit `codex.rollout_compression.file.duration_ms` tagged by file
    outcome.
    - Emit source and compressed byte histograms for compression
    candidates/results.
    - Emit `codex.rollout_compression.file.compression_ratio` for successful
    compressions, recorded as integer basis points.
    
    ## Validation
    
    - `just fmt`
    - `just test -p codex-rollout`
    - `just fix -p codex-rollout`
  • [codex] document out-of-line test module convention (#25682)
    ## Why
    
    New unit test modules should follow one consistent layout so
    implementation files stay focused and test suites remain easy to locate,
    without creating cleanup churn in existing inline test modules.
    
    ## What changed
    
    - Added `AGENTS.md` guidance requiring new test modules to use separate
    sibling `*_tests.rs` files with an explicit `#[path = "..._tests.rs"]`
    attribute.
    - Clarified that existing inline `#[cfg(test)] mod tests { ... }`
    modules should not be moved solely to follow the new convention.
    
    ## Validation
    
    - Ran `git diff --check`.
  • Add rollout compression counters (#25679)
    ## Summary
    
    Add counter telemetry for the local rollout compression worker so we can
    see when it runs, why it skips, and how individual file/materialization
    paths resolve.
    
    ## Changes
    
    - Emit `codex.rollout_compression.run` with statuses for start,
    completion, failure, duplicate-run skip, and missing runtime skip.
    - Emit `codex.rollout_compression.file` outcomes for scanned,
    compressed, skipped, and failed compression candidates.
    - Emit `codex.rollout_compression.temp_cleanup` and
    `codex.rollout_compression.materialize` counters for cleanup and
    decompression paths.
    
    ## Validation
    
    - `just fmt`
    - `just test -p codex-rollout`
    - `just fix -p codex-rollout`
  • refactor: hide shell override for zsh fork unified exec (#24980)
    ## Why
    
    When unified exec is configured to launch through the zsh fork, local
    commands should not let the model override the shell binary with the
    `shell` parameter. The configured zsh fork is the mechanism that makes
    `execv(2)` interception reliable, so exposing `shell` for local zsh-fork
    execution would create a confusing API surface and undermine the
    composition.
    
    Remote environments are different: zsh-fork interception is local-only,
    so remote unified-exec calls must keep direct unified-exec behavior and
    still expose `shell` when a remote environment can be selected.
    
    ## What Changed
    
    - Taught the `exec_command` schema builder to omit the `shell` parameter
    when requested.
    - Hid `shell` from the unified-exec tool schema only when zsh-fork
    unified exec applies to all selectable environments.
    - Kept `shell` visible when any remote environment can be targeted,
    because those calls run through direct unified exec.
    - Made unified exec choose the effective shell mode per selected
    environment: local environments keep zsh-fork mode, remote environments
    use direct mode.
    - Left direct unified-exec behavior unchanged, including support for
    model-specified shells there.
    
    ## Verification
    
    - Added schema coverage showing `exec_command` can hide `shell`.
    - Added planner coverage showing zsh-fork unified exec hides `shell` for
    local-only execution while direct unified exec still exposes it.
    - Added planner coverage showing `shell` remains visible when a remote
    environment is available.
    - Added handler coverage showing remote environments use direct
    unified-exec shell mode instead of zsh-fork mode.
    - Ran the focused `codex-core` shell-parameter and zsh-fork tests.
    
    
    
    
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/24980).
    * #24982
    * #24981
    * __->__ #24980
  • feat: gate unified exec zsh fork composition (#24979)
    ## Why
    
    `shell_zsh_fork` and unified exec need to remain independently
    controllable for enterprise rollouts, but we also need a third mode that
    composes them. That composed mode is intended to preserve unified exec
    command lifecycle support while letting the zsh fork provide more
    accurate `execv(2)` interception.
    
    Enabling `unified_exec_zsh_fork` by itself is intentionally not
    sufficient. It is a composition gate, not a dependency-enabling
    shortcut:
    
    - `unified_exec` selects the PTY-backed unified exec tool.
    - `shell_zsh_fork` opts into the zsh fork backend.
    - `unified_exec_zsh_fork` only allows those two already-enabled modes to
    be composed so local zsh unified exec commands can launch through the
    zsh fork.
    
    This separation is deliberate. Enterprises and staged rollouts must be
    able to enable or disable unified exec and zsh-fork independently. If
    `unified_exec_zsh_fork` implied either dependency, then enabling one
    under-development composition flag would silently activate a shell
    backend that the configured feature set left disabled.
    
    This PR introduces only the configuration and planning gate for that
    composition. Existing `shell_zsh_fork` behavior continues to use the
    standalone shell tool unless the new composition feature is explicitly
    enabled alongside both dependencies.
    
    ## What Changed
    
    - Added the under-development feature flag `unified_exec_zsh_fork`.
    - Added `UnifiedExecFeatureMode` so the three input feature flags
    collapse into `Disabled`, `Direct`, or `ZshFork` mode before tool
    planning.
    - Updated tool selection so zsh-fork composition requires
    `unified_exec`, `shell_zsh_fork`, and `unified_exec_zsh_fork`.
    - Kept the existing standalone zsh-fork shell tool behavior when only
    `shell_zsh_fork` is enabled.
    - Updated config schema output for the new feature flag.
    
    ## Verification
    
    - Added feature and tool-config coverage for the new gate.
    - Added planner coverage proving `shell_zsh_fork` remains standalone
    until composition is explicitly enabled.
    - Ran focused tests for `codex-features`, `codex-tools`, and the
    affected `codex-core` planner case.
    
    
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/24979).
    * #24982
    * #24981
    * #24980
    * __->__ #24979
  • fix: deflake zsh-fork approval test (#25669)
    Fixes this flake:
    https://github.com/openai/codex/actions/runs/26773809591/job/78919970410?pr=25659
    
    This test is about zsh-fork subcommand approval behavior, not workspace
    sandboxing, so it now runs with `DangerFullAccess` to avoid macOS
    sandbox setup failures before the second subcommand approval.
  • exec-server: canonicalize bound filesystem paths (#25149)
    ## Summary
    - add executor filesystem canonicalization as a bound-path operation
    - route remote canonicalization through the exec-server filesystem RPC
    surface
    - keep path normalization attached to the filesystem that owns the path
    
    ## Stack
    - 2/5 in the skills path authority stack extracted from
    https://github.com/openai/codex/pull/25098
    - follows merged https://github.com/openai/codex/pull/25121
    
    ## Validation
    - `cd
    /Users/starr/code/codex-worktrees/pr-25098-restack-review-pr1b/codex-rs
    && just fmt`
    - Not run: tests/checks (not requested)
    - GitHub CI pending on rewritten head
  • [codex-rs] auto-review model override (#23767)
    ## Why
    
    Guardian auto-review normally uses the provider-preferred review model
    when one is available. Some parent models need model-catalog metadata to
    select a different review model while keeping older `/models` payloads
    compatible when that metadata is absent.
    
    ## What changed
    
    - Added optional `ModelInfo::auto_review_model_override` metadata to the
    public model payload as a review-model slug.
    - Updated Guardian review model selection to prefer the catalog override
    when present, while preserving the existing provider preferred-model
    path and parent-model fallback when it is omitted.
    - Added focused Guardian coverage for override and no-override model
    selection.
    - Added an `auto_review` core integration suite test that loads override
    metadata from a remote model catalog path and asserts the strict
    auto-review `/responses` request uses the catalog-selected review model.
    - Updated existing `ModelInfo` fixtures and local catalog constructors
    for the new optional field.
    
    ## Validation
    
    - `cargo test -p codex-protocol
    model_info_defaults_availability_nux_to_none_when_omitted`
    - `cargo test -p codex-core guardian_review_uses_`
    - `cargo test -p codex-core
    remote_model_override_uses_catalog_model_for_strict_auto_review --test
    all`
    - `just fix -p codex-protocol`
    - `just fix -p codex-core`
    - `just fmt`
    - `git diff --check`