Commit Graph

2815 Commits

  • Add max context window model metadata (#18382)
    Adds max_context_window to model metadata and routes core context-window
    reads through resolved model info. Config model_context_window overrides
    are clamped to max_context_window when present; without an override, the
    model context_window is used.
  • [codex] Add marketplace remove command and shared logic (#17752)
    ## Summary
    
    Move the marketplace remove implementation into shared core logic so
    both the CLI command and follow-up app-server RPC can reuse the same
    behavior.
    
    This change:
    - adds a shared `codex_core::plugins::remove_marketplace(...)` flow
    - moves validation, config removal, and installed-root deletion out of
    the CLI
    - keeps the CLI as a thin wrapper over the shared implementation
    - adds focused core coverage for the shared remove path
    
    ## Validation
    
    - `just fmt`
    - focused local coverage for the shared remove path
    - heavier follow-up validation deferred to stacked PR CI
  • [codex] Describe uninstalled cross-repo plugin reads (#18449)
    ## Summary
    - Populate `PluginDetail.description` in core for uninstalled cross-repo
    plugins when detailed fields are unavailable until install.
    - Include the source Git URL plus optional path/ref/sha details in that
    fallback description.
    - Keep `details_unavailable_reason` as the structured signal while
    app-server forwards the description normally.
    - Add plugin-read coverage proving the response does not clone the
    remote source just to show the message.
    
    ## Why
    Uninstalled cross-repo plugins intentionally return sparse detail data
    so listing/reading does not clone the plugin source. Without a
    description, Desktop and TUI detail pages look like an ordinary empty
    plugin. This gives users a concrete explanation and source pointer while
    keeping the existing structured reason available for callers.
    
    ## Validation
    - `just fmt`
    - `cargo test -p codex-core
    read_plugin_for_config_uninstalled_git_source_requires_install_without_cloning`
    - `cargo test -p codex-app-server plugin_read --test all`
    - `just fix -p codex-core`
    - `just fix -p codex-app-server`
    
    Note: `cargo test -p codex-app-server` was also attempted before the
    latest refactor and failed broadly in unrelated v2
    thread/realtime/review/skills suites; the new plugin-read test passed in
    that run as well.
  • feat: Budget skill metadata and surface trimming as a warning (#18298)
    Cap the model-visible skills section to a small share of the context
    window, with a fallback character budget, and keep only as many implicit
    skills as fit within that budget.
    
    Emit a non-fatal warning when enabled skills are omitted, and add a new
    app-server warning notification
    
    Record thread-start skill metrics for total enabled skills, kept skills,
    and whether truncation happened
    
    ---------
    
    Co-authored-by: Matthew Zeng <mzeng@openai.com>
    Co-authored-by: Codex <noreply@openai.com>
  • [TUI] add external config migration prompt when start TUI (#17891)
    - add a TUI startup migration prompt for external agent config
    - support migrating external configs including config, skills, AGENTS.md
    and plugins
    - gate the prompt behind features.external_migrate (default false)
    
    <img width="1037" height="480" alt="Screenshot 2026-04-14 at 9 29 14 PM"
    src="https://github.com/user-attachments/assets/6060849b-03cb-429a-9c13-c7bb46ad2e65"
    />
    <img width="713" height="183" alt="Screenshot 2026-04-14 at 9 29 26 PM"
    src="https://github.com/user-attachments/assets/d13f177e-d4c4-479c-8736-ef29636081e1"
    />
    
    ---------
    
    Co-authored-by: Eric Traut <etraut@openai.com>
  • fix: trust-gate project hooks and exec policies (#14718)
    ## Summary
    - trust-gate project `.codex` layers consistently, including repos that
    have `.codex/hooks.json` or `.codex/execpolicy/*.rules` but no
    `.codex/config.toml`
    - keep disabled project layers in the config stack so nested trusted
    project layers still resolve correctly, while preventing hooks and exec
    policies from loading until the project is trusted
    - update app-server/TUI onboarding copy to make the trust boundary
    explicit and add regressions for loader, hooks, exec-policy, and
    onboarding coverage
    
    ## Security
    Before this change, an untrusted repo could auto-load project hooks or
    exec policies from `.codex/` as long as `config.toml` was absent. This
    makes trust the single gate for project-local config, hooks, and exec
    policies.
    
    ## Stack
    - Parent of #15936
    
    ## Test
    - cargo test -p codex-core without_config_toml
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • feat: Add remote plugin fields to plugin API (#17277)
    ## Summary
    Update the plugin API for the new remote plugin model.
    
    The mental model is no longer “keep local plugin state in sync with
    remote.” Instead, local and remote plugins are becoming separate
    sources. Remote catalog entries can be shown directly from the remote
    API before installation; after installation they are still downloaded
    into the local cache for execution, but remote installed state will come
    from the API and be held in memory rather than being read from config.
    
    • ## API changes
    - Remove `forceRemoteSync` from `plugin/list`, `plugin/install`, and
    `plugin/uninstall`.
      - Remove `remoteSyncError` from `plugin/list`.
      - Add remote-capable metadata to `plugin/list` / `plugin/read`:
        - nullable `marketplaces[].path`
        - `source: { type: "remote", downloadUrl }`
        - URL asset fields alongside local path fields:
      `composerIconUrl`, `logoUrl`, `screenshotUrls`
      - Make `plugin/read` and `plugin/install` source-compatible:
        - `marketplacePath?: AbsolutePathBuf | null`
        - `remoteMarketplaceName?: string | null`
        - exactly one source is required at runtime
  • Update image resizing to fit 2048 square bounds (#18384)
    We don't have to downsize to 768 height.
  • bazel: use native rust test sharding (#18082)
    ## Why
    
    The large Rust test suites are slow and include some of our flakiest
    tests, so we want to run them with Bazel native sharding while keeping
    shard membership stable between runs.
    
    This is the simpler follow-up to the explicit-label experiment in
    #17998. Since #18397 upgraded Codex to `rules_rs` `0.0.58`, which
    includes the stable test-name hashing support from
    hermeticbuild/rules_rust#14, this PR only needs to wire Codex's Bazel
    macros into that support.
    
    Using native sharding preserves BuildBuddy's sharded-test UI and Bazel's
    per-shard test action caching. Using stable name hashing avoids
    reshuffling every test when one test is added or removed.
    
    ## What Changed
    
    `codex_rust_crate` now accepts `test_shard_counts` and applies the right
    Bazel/rules_rust attributes to generated unit and integration test
    rules. Matched tests are also marked `flaky = True`, giving them Bazel's
    default three attempts.
    
    This PR shards these labels 8 ways:
    
    ```text
    //codex-rs/core:core-all-test
    //codex-rs/core:core-unit-tests
    //codex-rs/app-server:app-server-all-test
    //codex-rs/app-server:app-server-unit-tests
    //codex-rs/tui:tui-unit-tests
    ```
    
    ## Verification
    
    `bazel query --output=build` over the selected public labels and their
    inner unit-test binaries confirmed the expected `shard_count = 8`,
    `flaky = True`, and `experimental_enable_sharding = True` attributes.
    
    Also verified that we see the shards as expected in BuildBuddy so they
    can be analyzed independently.
    
    Co-authored-by: Codex <noreply@openai.com>
  • fix: fix fs sandbox helper for apply_patch (#18296)
    ## Summary
    
    - pass split filesystem sandbox policy/cwd through apply_patch contexts,
    while omitting legacy-equivalent policies to keep payloads small
    - keep the fs helper compatible with legacy Landlock by avoiding helper
    read-root permission expansion in that mode and disabling helper network
    access
    
    ## Root Cause
    
    `d626dc38950fb40a1a5ad0a8ffab2485e3348c53` routed exec-server filesystem
    operations through a sandboxed helper. That path forwarded legacy
    Landlock into a helper policy shape that could require direct
    split-policy enforcement. Sandboxed `apply_patch` hit that edge through
    the filesystem abstraction.
    
    The same 0.121 edit-regression path is consistent with #18354: normal
    writes route through the `apply_patch` filesystem helper, fail under
    sandbox, and then surface the generic retry-without-sandbox prompt.
    
    Fixes #18069
    Fixes #18354
    
    ## Validation
    
    - `cd codex-rs && just fmt`
    - earlier branch validation before merging current `origin/main` and
    dropping the now-separate PATH fix:
      - `cd codex-rs && cargo test -p codex-exec-server`
    - `cd codex-rs && cargo test -p codex-core file_system_sandbox_context`
      - `cd codex-rs && just fix -p codex-exec-server`
      - `cd codex-rs && just fix -p codex-core`
      - `git diff --check`
      - `cd codex-rs && cargo clean`
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • [codex] Add cross-repo plugin sources to marketplace manifests (#18017)
    ## Summary
    - add first-class marketplace support for git-backed plugin sources
    - keep the newer marketplace parsing behavior from `main`, including
    alternate manifest locations and string local sources
    - materialize remote plugin sources during install, detail reads, and
    non-curated cache refresh
    - expose git plugin source metadata through the app-server protocol
    
    ## Details
    This teaches the marketplace parser to accept all of the following:
    - local string sources such as `"source": "./plugins/foo"`
    - local object sources such as
    `{"source":"local","path":"./plugins/foo"}`
    - remote repo-root sources such as
    `{"source":"url","url":"https://github.com/org/repo.git"}`
    - remote subdir sources such as
    `{"source":"git-subdir","url":"owner/repo","path":"plugins/foo","ref":"main","sha":"..."}`
    
    It also preserves the newer tolerant behavior from `main`: invalid or
    unsupported plugin entries are skipped instead of breaking the whole
    marketplace.
    
    ## Validation
    - `cargo test -p codex-core plugins::marketplace::tests`
    - `just fix -p codex-core`
    - `just fmt`
    
    ## Notes
    - A full `cargo test -p codex-core` run still hit unrelated existing
    failures in agent and multi-agent tests during this session; the
    marketplace-focused suite passed after the rebase resolution.
  • refactor: narrow async lock guard lifetimes (#18211)
    Follow-up to https://github.com/openai/codex/pull/18178, where we called
    out enabling the await-holding lint as a follow-up.
    
    The long-term goal is to enable Clippy coverage for async guards held
    across awaits. This PR is intentionally only the first, low-risk cleanup
    pass: it narrows obvious lock guard lifetimes and leaves
    `codex-rs/Cargo.toml` unchanged so the lint is not enabled until the
    remaining cases are fixed or explicitly justified. It intentionally
    leaves the active-turn/turn-state locking pattern alone because those
    checks and mutations need to stay atomic.
    
    ## Common fixes used here
    
    These are the main patterns reviewers should expect in this PR, and they
    are also the patterns to reach for when fixing future `await_holding_*`
    findings:
    
    - **Scope the guard to the synchronous work.** If the code only needs
    data from a locked value, move the lock into a small block, clone or
    compute the needed values, and do the later `.await` after the block.
    - **Use direct one-line mutations when there is no later await.** Cases
    like `map.lock().await.remove(&id)` are acceptable when the guard is
    only needed for that single mutation and the statement ends before any
    async work.
    - **Drain or clone work out of the lock before notifying or awaiting.**
    For example, the JS REPL drains pending exec senders into a local vector
    and the websocket writer clones buffered envelopes before it serializes
    or sends them.
    - **Use a `Semaphore` only when serialization is intentional across
    async work.** The test serialization guards intentionally span awaited
    setup or execution, so using a semaphore communicates "one at a time"
    without holding a mutex guard.
    - **Remove the mutex when there is only one owner.** The PTY stdin
    writer task owns `stdin` directly; the old `Arc<Mutex<_>>` did not
    protect shared access because nothing else had access to the writer.
    - **Do not split locks that protect an atomic invariant.** This PR
    deliberately leaves active-turn/turn-state paths alone because those
    checks and mutations need to stay atomic. Those cases should be fixed
    separately with a design change or documented with `#[expect]`.
    
    ## What changed
    
    - Narrow scoped async mutex guards in app-server, JS REPL, network
    approval, remote-control websocket, and the RMCP test server.
    - Replace test-only async mutex serialization guards with semaphores
    where the guard intentionally lives across async work.
    - Let the PTY pipe writer task own stdin directly instead of wrapping it
    in an async mutex.
    
    ## Verification
    
    - `just fix -p codex-core -p codex-app-server -p codex-rmcp-client -p
    codex-shell-escalation -p codex-utils-pty -p codex-utils-readiness`
    - `just clippy -p codex-core`
    - `cargo test -p codex-core -p codex-app-server -p codex-rmcp-client -p
    codex-shell-escalation -p codex-utils-pty -p codex-utils-readiness` was
    run; the app-server suite passed, and `codex-core` failed in the local
    sandbox on six otel approval tests plus
    `suite::user_shell_cmd::user_shell_command_does_not_set_network_sandbox_env_var`,
    which appear to depend on local command approval/default rules and
    `CODEX_SANDBOX_NETWORK_DISABLED=1` in this environment.
  • [codex] Propagate rate limit reached type (#18227)
    ## Summary
    
    First PR in the split from #17956.
    
    - adds the core/app-server `RateLimitReachedType` shape
    - maps backend `rate_limit_reached_type` into Codex rate-limit snapshots
    - carries the field through app-server notifications/responses and
    generated schemas
    - updates existing constructors/tests for the new optional field
    
    ## Validation
    
    - `cargo test -p codex-backend-client`
    - `cargo test -p codex-app-server-protocol`
    - `cargo test -p codex-app-server rate_limits`
    - `cargo test -p codex-tui workspace_`
    - `cargo test -p codex-tui status_`
    - `just fmt`
    - `just fix -p codex-backend-client`
    - `just fix -p codex-app-server-protocol`
    - `just fix -p codex-app-server`
    - `just fix -p codex-tui`
  • Update models.json (#12640)
    Automated update of models.json.
    
    Co-authored-by: aibrahim-oai <219906144+aibrahim-oai@users.noreply.github.com>
    Co-authored-by: Ahmed Ibrahim <aibrahim@openai.com>
  • [3/6] Add pushed exec process events (#18020)
    ## Summary
    - Add a pushed `ExecProcessEvent` stream alongside retained
    `process/read` output.
    - Publish local and remote output, exit, close, and failure events.
    - Cover the event stream with shared local/remote exec process tests.
    
    ## Testing
    - `cargo check -p codex-exec-server`
    - `cargo check -p codex-rmcp-client`
    - Not run: `cargo test` per repo instruction; CI will cover.
    
    ## Stack
    ```text
    o  #18027 [6/6] Fail exec client operations after disconnect
    │
    o  #18212 [5/6] Wire executor-backed MCP stdio
    │
    o  #18087 [4/6] Abstract MCP stdio server launching
    │
    @  #18020 [3/6] Add pushed exec process events
    │
    o  #18086 [2/6] Support piped stdin in exec process API
    │
    o  #18085 [1/6] Add MCP server environment config
    │
    o  main
    ```
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Add sorting/backwardsCursor to thread/list and new thread/turns/list api (#17305)
    To improve performance of UI loads from the app, add two main
    improvements:
    1. The `thread/list` api now gets a `sortDirection` request field and a
    `backwardsCursor` to the response, which lets you paginate forwards and
    backwards from a window. This lets you fetch the first few items to
    display immediately while you paginate to fill in history, then can
    paginate "backwards" on future loads to catch up with any changes since
    the last UI load without a full reload of the entire data set.
    2. Added a new `thread/turns/list` api which also has sortDirection and
    backwardsCursor for the same behavior as `thread/list`, allowing you the
    same small-fetch for immediate display followed by background fill-in
    and resync catchup.
  • ci: make Windows Bazel clippy catch core test imports (#18350)
    ## Why
    
    Unused imports in `core/tests/suite/unified_exec.rs` in the Windows
    build were not caught by Bazel CI on
    https://github.com/openai/codex/pull/18096. I spot-checked
    https://github.com/openai/codex/actions/workflows/rust-ci-full.yml?query=branch%3Amain
    and noticed that builds were consistently red. This revealed that our
    Cargo builds _were_ properly catching these issues, identifying a
    Windows-specific coverage hole in the Bazel clippy job.
    
    The Windows Bazel clippy job uses `--skip_incompatible_explicit_targets`
    so it can lint a broad target set without failing immediately on targets
    that are genuinely incompatible with Windows. However, with the default
    Windows host platform, `rust_test` targets such as
    `//codex-rs/core:core-all-test` could be skipped before the clippy
    aspect reached their integration-test modules. As a result, the imports
    in `core/tests/suite/unified_exec.rs` were not being linted by the
    Windows Bazel clippy job at all.
    
    The clippy diagnostic that Windows Bazel should have surfaced was:
    
    ```text
    error: unused import: `codex_config::Constrained`
     --> core\tests\suite\unified_exec.rs:8:5
      |
    8 | use codex_config::Constrained;
      |     ^^^^^^^^^^^^^^^^^^^^^^^^^
      |
      = note: `-D unused-imports` implied by `-D warnings`
      = help: to override `-D warnings` add `#[allow(unused_imports)]`
    
    error: unused import: `codex_protocol::permissions::FileSystemAccessMode`
      --> core\tests\suite\unified_exec.rs:11:5
       |
    11 | use codex_protocol::permissions::FileSystemAccessMode;
       |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    
    error: unused import: `codex_protocol::permissions::FileSystemPath`
      --> core\tests\suite\unified_exec.rs:12:5
       |
    12 | use codex_protocol::permissions::FileSystemPath;
       |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    
    error: unused import: `codex_protocol::permissions::FileSystemSandboxEntry`
      --> core\tests\suite\unified_exec.rs:13:5
       |
    13 | use codex_protocol::permissions::FileSystemSandboxEntry;
       |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    
    error: unused import: `codex_protocol::permissions::FileSystemSandboxPolicy`
      --> core\tests\suite\unified_exec.rs:14:5
       |
    14 | use codex_protocol::permissions::FileSystemSandboxPolicy;
       |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    ```
    
    ## What changed
    
    - Run the Windows Bazel clippy job with the MSVC host platform via
    `--windows-msvc-host-platform`, matching the Windows Bazel test job.
    This keeps `--skip_incompatible_explicit_targets` while ensuring Windows
    `rust_test` targets such as `//codex-rs/core:core-all-test` are still
    linted.
    - Remove the unused imports from `core/tests/suite/unified_exec.rs`.
    - Add `--print-failed-action-summary` to
    `.github/scripts/run-bazel-ci.sh` so Bazel action failures can be
    summarized after the build exits.
    
    ## Failure reporting
    
    Once the coverage issue was fixed, an intentionally reintroduced unused
    import made the Windows Bazel clippy job fail as expected. That exposed
    a separate usability problem: because the job keeps `--keep_going`, the
    top-level Bazel output could still end with:
    
    ```text
    ERROR: Build did NOT complete successfully
    FAILED:
    ```
    
    without the underlying rustc/clippy diagnostic being visible in the
    obvious part of the GitHub Actions log.
    
    To keep `--keep_going` while making failures actionable, the wrapper now
    scans the captured Bazel console output for failed actions and prints
    the matching rustc/clippy diagnostic block. When a diagnostic block is
    found, it is emitted both as a GitHub `::error` annotation and as plain
    expanded log output, rather than being hidden in a collapsed group.
    
    ## Verification
    
    To validate the CI path, I intentionally introduced an unused import in
    `core/tests/suite/unified_exec.rs`. The Windows Bazel clippy job failed
    as expected, confirming that the integration-test module is now covered
    by Bazel clippy. The same failure also verified that the wrapper
    surfaces the matching clippy diagnostics directly in the Actions output.
  • enable tool search over dynamic tools (#18263)
    ## Summary
    
    - Normalize deferred MCP and dynamic tools into `ToolSearchEntry` values
    before constructing `ToolSearchHandler`.
    - Move the tool-search entry adapter out of `tools/handlers` and into
    `tools/tool_search_entry.rs` so the handlers directory stays focused on
    handlers.
    - Keep `ToolSearchHandler` operating over one generic entry list for
    BM25 search, namespace grouping, and per-bucket default limits.
    
    ## Why
    
    Follow-up cleanup for #17849. The dynamic tool-search support made the
    handler juggle source-specific MCP and dynamic tool lists, index
    arithmetic, output conversion, and namespace emission. This keeps source
    adaptation outside the handler so the search loop itself is smaller and
    source-agnostic.
    
    ## Validation
    
    - `just fmt`
    - `cargo test -p codex-core tools::handlers::tool_search::tests`
    - `git diff --check`
    - `cargo test -p codex-core` currently fails in unrelated
    `plugins::manager::tests::list_marketplaces_ignores_installed_roots_missing_from_config`;
    rerunning that single test fails the same way at
    `core/src/plugins/manager_tests.rs:1692`.
    
    ---------
    
    Co-authored-by: pash <pash@openai.com>
  • Make app tool hint defaults pessimistic for app policies (#17232)
    ## Summary
    - default missing app tool destructive/open-world hints to true for app
    policies
    - add regression tests for missing MCP annotations under restrictive app
    config
  • feat: config aliases (#18140)
    Rename `no_memories_if_mcp_or_web_search` →
    `disable_on_external_context` with backward compatibility
    
    While doing so, we add a key alias system on our layer merging system.
    What we try to avoid is a case where a company managed config use an old
    name while the user has a new name in it's local config (which would
    make the deserialization fail)
  • Guardian -> Auto-Review (#18021)
    This PR is a user-facing change for our rebranding of guardian to
    auto-review.
  • Fix config-loader tests after filesystem abstraction race (#18351)
    ## Why
    
    `origin/main` picked up two changes that crossed in flight:
    
    - #18209 refactored config loading to read through `ExecutorFileSystem`,
    changing `load_requirements_toml` to take a filesystem handle and an
    `AbsolutePathBuf`.
    - #17740 added managed `deny_read` requirements tests that still called
    `load_requirements_toml` with the previous two-argument signature.
    
    Once both landed, `just clippy` failed because the new tests no longer
    matched the current helper API.
    
    ## What
    
    - Updates the two managed `deny_read` requirements tests to convert the
    fixture path to `AbsolutePathBuf` before loading.
    - Passes `LOCAL_FS.as_ref()` into `load_requirements_toml` so these
    tests follow the filesystem abstraction introduced by #18209.
    
    ## Verification
    
    - `just clippy`
    - `cargo test -p codex-core load_requirements_toml_resolves_deny_read`
    - `cargo test -p codex-core --test all
    unified_exec_enforces_glob_deny_read_policy`
  • Move codex module under session (#18249)
    ## Summary
    - rename the core codex module root to session/mod.rs without using
    #[path]
    - move the codex module directory and tests under core/src/session
    - remove session/mod.rs reexports so call sites use explicit child
    module paths
    
    ## Testing
    - cargo test -p codex-core --lib
    - cargo check -p codex-core --tests
    - just fmt
    - just fix -p codex-core
    - git diff --check
  • feat(config): support managed deny-read requirements (#17740)
    ## Summary
    - adds managed requirements support for deny-read filesystem entries
    - constrains config layers so managed deny-read requirements cannot be
    widened by user-controlled config
    - surfaces managed deny-read requirements through debug/config plumbing
    
    This PR lets managed requirements inject deny-read filesystem
    constraints into the effective filesystem sandbox policy.
    User-controlled config can still choose the surrounding permission
    profile, but it cannot remove or weaken the managed deny-read entries.
    
    ## Managed deny-read shape
    A managed requirements file can declare exact paths and glob patterns
    under `[permissions.filesystem]`:
    
    ```toml
    # /etc/codex/requirements.toml
    [permissions.filesystem]
    deny_read = [
      "/Users/alice/.gitconfig",
      "/Users/alice/.ssh",
      "./managed-private/**/*.env",
    ]
    ```
    
    Those entries are compiled into the effective filesystem policy as
    `access = none` rules, equivalent in shape to filesystem permission
    entries like:
    
    ```toml
    [permissions.workspace.filesystem]
    "/Users/alice/.gitconfig" = "none"
    "/Users/alice/.ssh" = "none"
    "/absolute/path/to/managed-private/**/*.env" = "none"
    ```
    
    The important difference is that the managed entries come from
    requirements, so lower-precedence user config cannot remove them or make
    those paths readable again.
    
    Relative managed `deny_read` entries are resolved relative to the
    directory containing the managed requirements file. Glob entries keep
    their glob suffix after the non-glob prefix is normalized.
    
    ## Runtime behavior
    - Managed `deny_read` entries are appended to the effective
    `FileSystemSandboxPolicy` after the selected permission profile is
    resolved.
    - Exact paths become `FileSystemPath::Path { access: None }`; glob
    patterns become `FileSystemPath::GlobPattern { access: None }`.
    - When managed deny-read entries are present, `sandbox_mode` is
    constrained to `read-only` or `workspace-write`; `danger-full-access`
    and `external-sandbox` cannot silently bypass the managed read-deny
    policy.
    - On Windows, the managed deny-read policy is enforced for direct file
    tools, but shell subprocess reads are not sandboxed yet, so startup
    emits a warning for that platform.
    - `/debug-config` shows the effective managed requirement as
    `permissions.filesystem.deny_read` with its source.
    
    ## Stack
    1. #15979 - glob deny-read policy/config/direct-tool support
    2. #18096 - macOS and Linux sandbox enforcement
    3. This PR - managed deny-read requirements
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Add PermissionRequest hooks support (#17563)
    ## Why
    
    We need `PermissionRequest` hook support!
    
    Also addresses:
    - https://github.com/openai/codex/issues/16301
    - run a script on Hook to do things like play a sound to draw attention
    but actually no-op so user can still approve
    - can omit the `decision` object from output or just have the script
    exit 0 and print nothing
    - https://github.com/openai/codex/issues/15311
      - let the script approve/deny on its own
      - external UI what will run on Hook and relay decision back to codex
    
    
    ## Reviewer Note
    
    There's a lot of plumbing for the new hook, key files to review are:
    - New hook added in `codex-rs/hooks/src/events/permission_request.rs`
    - Wiring for network approvals
    `codex-rs/core/src/tools/network_approval.rs`
    - Wiring for tool orchestrator `codex-rs/core/src/tools/orchestrator.rs`
    - Wiring for execve
    `codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs`
    
    ## What
    
    - Wires shell, unified exec, and network approval prompts into the
    `PermissionRequest` hook flow.
    - Lets hooks allow or deny approval prompts; quiet or invalid hooks fall
    back to the normal approval path.
    - Uses `tool_input.description` for user-facing context when it helps:
      - shell / `exec_command`: the request justification, when present
      - network approvals: `network-access <domain>`
    - Uses `tool_name: Bash` for shell, unified exec, and network approval
    permission-request hooks.
    - For network approvals, passes the originating command in
    `tool_input.command` when there is a single owning call; otherwise falls
    back to the synthetic `network-access ...` command.
    
    <details>
    <summary>Example `PermissionRequest` hook input for a shell
    approval</summary>
    
    ```json
    {
      "session_id": "<session-id>",
      "turn_id": "<turn-id>",
      "transcript_path": "/path/to/transcript.jsonl",
      "cwd": "/path/to/cwd",
      "hook_event_name": "PermissionRequest",
      "model": "gpt-5",
      "permission_mode": "default",
      "tool_name": "Bash",
      "tool_input": {
        "command": "rm -f /tmp/example"
      }
    }
    ```
    
    </details>
    
    <details>
    <summary>Example `PermissionRequest` hook input for an escalated
    `exec_command` request</summary>
    
    ```json
    {
      "session_id": "<session-id>",
      "turn_id": "<turn-id>",
      "transcript_path": "/path/to/transcript.jsonl",
      "cwd": "/path/to/cwd",
      "hook_event_name": "PermissionRequest",
      "model": "gpt-5",
      "permission_mode": "default",
      "tool_name": "Bash",
      "tool_input": {
        "command": "cp /tmp/source.json /Users/alice/export/source.json",
        "description": "Need to copy a generated file outside the workspace"
      }
    }
    ```
    
    </details>
    
    <details>
    <summary>Example `PermissionRequest` hook input for a network
    approval</summary>
    
    ```json
    {
      "session_id": "<session-id>",
      "turn_id": "<turn-id>",
      "transcript_path": "/path/to/transcript.jsonl",
      "cwd": "/path/to/cwd",
      "hook_event_name": "PermissionRequest",
      "model": "gpt-5",
      "permission_mode": "default",
      "tool_name": "Bash",
      "tool_input": {
        "command": "curl http://codex-network-test.invalid",
        "description": "network-access http://codex-network-test.invalid"
      }
    }
    ```
    
    </details>
    
    ## Follow-ups
    
    - Implement the `PermissionRequest` semantics for `updatedInput`,
    `updatedPermissions`, `interrupt`, and suggestions /
    `permission_suggestions`
    - Add `PermissionRequest` support for the `request_permissions` tool
    path
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • add token-based tool deferral behind feature flag (#18097)
    add new `tool_search_always_defer_mcp_tools` feature flag that always
    defers all mcp tools rather than deferring once > 100 deferrable tools.
    
    add new tests, also move `mcp_exposure` tests into dedicated file rather
    than polluting `codex_tests`.
  • Sync local plugin imports, async remote imports, refresh caches after… (#18246)
    … import
    
    ## Why
    
    `externalAgentConfig/import` used to spawn plugin imports in the
    background and return immediately. That meant local marketplace imports
    could still be in flight when the caller refreshed plugin state, so
    newly imported plugins would not show up right away.
    
    This change makes local marketplace imports complete before the RPC
    returns, while keeping remote marketplace imports asynchronous so we do
    not block on remote fetches.
    
    ## What changed
    
    - split plugin migration details into local and remote marketplace
    imports based on the external config source
    - import local marketplaces synchronously during
    `externalAgentConfig/import`
    - return pending remote plugin imports to the app-server so it can
    finish them in the background
    - clear the plugin and skills caches before responding to plugin
    imports, and again after background remote imports complete, so the next
    `plugin/list` reloads fresh state
    - keep marketplace source parsing encapsulated behind
    `is_local_marketplace_source(...)` instead of re-exporting the internal
    enum
    - add core and app-server coverage for the synchronous local import path
    and the pending remote import path
    
    ## Verification
    
    - `cargo test -p codex-app-server-protocol`
    - `cargo test -p codex-core` (currently fails an existing unrelated
    test:
    `config_loader::tests::cli_override_can_update_project_local_mcp_server_when_project_is_trusted`)
    - `cargo test` (currently fails existing `codex-app-server` integration
    tests in MCP/skills/thread-start areas, plus the unrelated `codex-core`
    failure above)
  • Fix Windows exec policy test flake (#18304)
    ## Summary
    
    This fixes a Windows-only failure in the exec policy multi-segment shell
    test. The test was meant to verify that a compound shell command only
    bypasses sandboxing when every parsed segment has an explicit exec
    policy allow rule.
    
    On Windows, the read-only sandbox setup is intentionally treated as
    lacking sandbox protection, so the old fixture could take the approval
    path before reaching the intended bypass assertion. The test now uses
    the workspace-write sandbox policy, keeping the focus on the per-segment
    bypass rule while preserving the expected bypass_sandbox false result
    when only cat is explicitly allowed.
  • fix(exec-policy) rules parsing (#18126)
    ## Summary
    See scenarios - rules must always be enforced on all commands in the
    string
    
    ## Testing
    - [x] Added ExecApprovalRequirementScenario tests
  • Move Computer Use tool suggestion to core (#18219)
    ## Summary
    
    Move the Computer Use tool suggestion into core Codex plugin discovery.
    
    Also search `openai-bundled` when listing suggested plugins, with test
    coverage for overlap between baked-in suggestions and
    `tool_suggest.discoverables`.
    
    ## Test plan
    
    Tested locally:
    
    - `cargo test -p codex-core list_tool_suggest_discoverable_plugins`
  • feat: Handle alternate plugin manifest paths (#18182)
    Load plugin manifests through a shared discoverable-path helper so
    manifest reads, installs, and skill names all see the same alternate
    manifest location.
  • feat: add opt-in provider runtime abstraction (#17713)
    ## Summary
    
    - Add `codex-model-provider` as the runtime home for model-provider
    behavior that does not belong in `codex-core`, `codex-login`, or
    `codex-api`.
    - The new crate wraps configured `ModelProviderInfo` in a
    `ModelProvider` trait object that can resolve the API provider config,
    provider-scoped auth manager, and request auth provider for each call.
    - This centralizes provider auth behavior in one place today, and gives
    us an extension point for future provider-specific auth, model listing,
    request setup, and related runtime behavior.
    
    ## Tests
    Ran tests manually to make sure that provider auth under different
    configs still work as expected.
    
    ---------
    
    Co-authored-by: pakrym-oai <pakrym@openai.com>
  • Split codex session modules (#18244)
    ## Summary
    - split `codex.rs` session definitions and constructor into
    `codex/session.rs`
    - move MCP session methods into `codex/mcp.rs`
    - move turn-context types/helpers into `codex/turn_context.rs`
    - move review thread spawning into `codex/review.rs`
    
    ## Testing
    - `cargo check -p codex-core`
    - `just fmt`
    - `just fix -p codex-core`
    - `cargo test -p codex-core` (unit tests passed; integration run failed
    locally with 45 failures, including missing helper binaries such as
    `test_stdio_server`/`codex` plus approval/web-search/MCP-related cases)
  • Stream apply_patch changes (#17862)
    Adds new events for streaming apply_patch changes from responses api.
    This is to enable clients to show progress during file writes.
    
    Caveat: This does not work with apply_patch in function call mode, since
    that required adding streaming json parsing.
  • Refactor config loading to use filesystem abstraction (#18209)
    Initial pass propagating FileSystem through config loading.
  • fix: deprecate use_legacy_landlock feature flag (#17971)
    ## Summary
    - mark `features.use_legacy_landlock` as a deprecated feature flag
    - emit a startup deprecation notice when the flag is configured
    - add feature- and core-level regression coverage for the notice
    
    
    <img width="1288" height="93" alt="Screenshot 2026-04-15 at 11 14 00 PM"
    src="https://github.com/user-attachments/assets/fffc628b-614c-4521-9374-64e50a269252"
    />
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • feat(sandbox): add glob deny-read platform enforcement (#18096)
    ## Summary
    - adds macOS Seatbelt deny rules for unreadable glob patterns
    - expands unreadable glob matches on Linux and masks them in bwrap,
    including canonical symlink targets
    - keeps Linux glob expansion robust when `rg` is unavailable in minimal
    or Bazel test environments
    - adds sandbox integration coverage that runs `shell` and `exec_command`
    with a `**/*.env = none` policy and verifies the secret contents do not
    reach the model
    
    ## Linux glob expansion
    
    ```text
    Prefer:   rg --files --hidden --no-ignore --glob <pattern> -- <search-root>
    Fallback: internal globset walker when rg is not installed
    Failure:  any other rg failure aborts sandbox construction
    ```
    
    ```
    [permissions.workspace.filesystem]
    glob_scan_max_depth = 2
    
    [permissions.workspace.filesystem.":project_roots"]
    "**/*.env" = "none"
    ```
    
    
    This keeps the common path fast without making sandbox construction
    depend on an ambient `rg` binary. If `rg` is present but fails for
    another reason, the sandbox setup fails closed instead of silently
    omitting deny-read masks.
    
    ## Platform support
    - macOS: subprocess sandbox enforcement is handled by Seatbelt regex
    deny rules
    - Linux: subprocess sandbox enforcement is handled by expanding existing
    glob matches and masking them in bwrap
    - Windows: policy/config/direct-tool glob support is already on `main`
    from #15979; Windows subprocess sandbox paths continue to fail closed
    when unreadable split filesystem carveouts require runtime enforcement,
    rather than silently running unsandboxed
    
    ## Stack
    1. #15979 - merged: cross-platform glob deny-read
    policy/config/direct-tool support for macOS, Linux, and Windows
    2. This PR - macOS/Linux subprocess sandbox enforcement plus Windows
    fail-closed clarification
    3. #17740 - managed deny-read requirements
    
    ## Verification
    - Added integration coverage for `shell` and `exec_command` glob
    deny-read enforcement
    - `cargo check -p codex-sandboxing -p codex-linux-sandbox --tests`
    - `cargo check -p codex-core --test all`
    - `cargo clippy -p codex-linux-sandbox -p codex-sandboxing --tests`
    - `just bazel-lock-check`
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Move marketplace add under plugin command (#18116)
    ## Summary
    - move the marketplace add CLI from `codex marketplace add` to `codex
    plugin marketplace add`
    - keep marketplace config overrides working through the nested plugin
    command
    - reject `--sparse` for local marketplace directory sources before the
    local-source install path bypasses git-source validation
    
    ## Validation
    - `just fmt`
    - `git diff --check`
    - `cargo test -p codex-cli`
    - `cargo test -p codex-core marketplace_add -- --nocapture`
    - `cargo test -p codex-core
    install_plugin_updates_config_with_relative_path_and_plugin_key --
    --nocapture`
    - `xli-test-marketplace-cli` local isolated matrix: `T1`, `L1`-`L10`
  • Use codex-auto-review for guardian reviews (#18169)
    ## Summary
    
    This is the minimal client-side follow-up for the Codex Auto Review
    model slug rollout. It updates the guardian reviewer preferred model
    from `gpt-5.4` to `codex-auto-review`, so the client can rely on the
    backend catalog + Statsig mapping instead of hardcoding the GPT-5.4
    slug.
    
    Context:
    https://openai.slack.com/archives/C0AF9328RL0/p1775777479388369?thread_ts=1775773094.071629&cid=C0AF9328RL0
    
    ## Testing
    
    - `cargo fmt --package codex-core --check`
    - `cargo test -p codex-core guardian::`
    - `bazel test --experimental_remote_downloader= --test_output=errors
    //codex-rs/core:core-unit-tests --test_arg=guardian`
  • [codex] Split codex turn logic (#18206)
    ## Summary
    - Move Codex turn execution logic from `codex.rs` into `codex/turn.rs`.
    - Keep the existing crate-visible `run_turn`, `build_prompt`,
    `built_tools`, and `get_last_assistant_message_from_turn` surface
    re-exported from `codex.rs`.
    - Preserve test access for moved turn helpers while reducing the main
    `codex.rs` orchestration footprint.
    
    ## Stack
    - Base: #18200 (`pakrym/split-codex-handlers`)
    
    ## Testing
    - `CARGO_INCREMENTAL=0 cargo test -p codex-core --lib`
    - `just fix -p codex-core`
    - `just fmt`
    - `git diff --check`
  • [codex] Update realtime V2 VAD silence delay and 1.5 prompt (#18092)
    ## Summary
    
    - set the realtime v2 server VAD silence delay to 500ms
    - update the default realtime 1.5 backend prompt to the v4 text
    - keep the session payload and prompt rendering tests aligned with those
    changes
    
    ## Why
    
    - the VAD change gives the voice path a longer pause before ending the
    user's turn
    - the prompt change makes the default bundled realtime prompt match the
    current v4 content
    
    ## Validation
    
    - `cargo +1.93.0 test -p codex-core realtime_prompt --manifest-path
    /tmp/codex-realtime-v2-vad-prompt-v4/codex-rs/Cargo.toml`
    - `CARGO_TARGET_DIR=/tmp/codex-pr-v4-target cargo +1.93.0 test -p
    codex-api
    realtime_v2_session_update_includes_background_agent_tool_and_handoff_output_item
    --manifest-path
    /tmp/codex-realtime-v2-vad-prompt-v4/codex-rs/Cargo.toml`
    - `CARGO_TARGET_DIR=/tmp/codex-pr-v4-target cargo +1.93.0 test -p
    codex-app-server --test all
    'suite::v2::realtime_conversation::realtime_webrtc_start_emits_sdp_notification'
    --manifest-path /tmp/codex-realtime-v2-vad-prompt-v4/codex-rs/Cargo.toml
    -- --exact`
  • Add OTEL metrics for hook runs (#18026)
    # Why
    We already emit analytics for completed hook runs, but we don't have
    matching OTEL metrics to track hook volume and latency.
    
    # What
    - add `codex.hooks.run` and `codex.hooks.run.duration_ms`
    - tag both metrics with `hook_name`, `source`, and `status`
    - emit the metrics from the completed hook path
    
    Verified locally against a dummy OTLP collector
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Register agent tasks behind use_agent_identity (#17387)
    ## Summary
    
    Stack PR3 for feature-gated agent identity support.
    
    This PR adds per-thread agent task registration behind
    `features.use_agent_identity`. Tasks are minted on the first real user
    turn and cached in thread runtime state for later turns.
    
    ## Stack
    
    - PR1: https://github.com/openai/codex/pull/17385 - add
    `features.use_agent_identity`
    - PR2: https://github.com/openai/codex/pull/17386 - register agent
    identities when enabled
    - PR3: https://github.com/openai/codex/pull/17387 - this PR, original
    task registration slice
    - PR3.1: https://github.com/openai/codex/pull/17978 - persist and
    prewarm registered tasks per thread
    - PR4: https://github.com/openai/codex/pull/17980 - use `AgentAssertion`
    downstream when enabled
    
    ## Validation
    
    Covered as part of the local stack validation pass:
    
    - `just fmt`
    - `cargo test -p codex-core --lib agent_identity`
    - `cargo test -p codex-core --lib agent_assertion`
    - `cargo test -p codex-core --lib websocket_agent_task`
    - `cargo test -p codex-api api_bridge`
    - `cargo build -p codex-cli --bin codex`
    
    ## Notes
    
    The full local app-server E2E path is still being debugged after PR
    creation. The current branch stack is directionally ready for review
    while that follow-up continues.
  • [codex] Split codex op handlers (#18200)
    Start splitting the codex.rs
  • [codex] Make realtime startup context truncation deterministic (#18172)
    ## Summary
    
    - remove the final whole-blob truncation pass from realtime
    startup-context assembly
    - enforce fixed per-section budgets, including each section heading
    - keep the existing per-section caps and raise the overall realtime
    startup-context budget to `5300`, matching the sum of those section
    budgets
    - add focused tests for the new wrapping and section-budget behavior
    
    ## Why
    
    The previous flow truncated each section and then middle-truncated the
    final combined startup-context blob again. Small input changes could
    shift that combined cut point, which made retained context unstable and
    caused nondeterministic tests.
    
    ## Impact
    
    Startup context now preserves section boundaries and ordering
    deterministically. Each section is still budgeted independently, but the
    final assembled blob is no longer truncated again as a single opaque
    string. To match that design, the overall startup-context token budget
    is updated to the sum of the existing section budgets rather than
    lowering the section caps.
    
    ## Validation
    
    - `cargo +1.93.0 test -p codex-core realtime_context`
    - `cargo +1.93.0 test -p codex-core --test all
    suite::realtime_conversation::conversation_start_injects_startup_context_from_thread_history
    -- --exact`
    - `cargo +1.93.0 test -p codex-core --test all
    suite::realtime_conversation::conversation_startup_context_current_thread_selects_many_turns_by_budget
    -- --exact`
    - `cargo +1.93.0 test -p codex-core --test all
    suite::realtime_conversation::conversation_startup_context_falls_back_to_workspace_map
    -- --exact`
    - `cargo +1.93.0 test -p codex-core --test all
    suite::realtime_conversation::conversation_startup_context_is_truncated_and_sent_once_per_start
    -- --exact`
  • fix(app-server): replay token usage after resume and fork (#18023)
    ## Problem
    
    When a user resumed or forked a session, the TUI could render the
    restored thread history immediately, but it did not receive token usage
    until a later model turn emitted a fresh usage event. That left the
    context/status UI blank or stale during the exact window where the user
    expects resumed state to look complete. Core already reconstructed token
    usage from the rollout; the missing behavior was app-server lifecycle
    replay to the client that just attached.
    
    ## Mental model
    
    Token usage has two representations. The rollout is the durable source
    of historical `TokenCount` events, and the core session cache is the
    in-memory snapshot reconstructed from that rollout on resume or fork.
    App-server v2 clients do not read core state directly; they learn about
    usage through `thread/tokenUsage/updated`. The fix keeps those roles
    separate: core exposes the restored `TokenUsageInfo`, and app-server
    sends one targeted notification after a successful `thread/resume` or
    `thread/fork` response when that restored snapshot exists.
    
    This notification is not a new model event. It is a replay of
    already-persisted state for the client that just attached. That
    distinction matters because using the normal core event path here would
    risk duplicating `TokenCount` entries in the rollout and making future
    resumes count historical usage twice.
    
    ## Non-goals
    
    This change does not add a new protocol method or payload shape. It
    reuses the existing v2 `thread/tokenUsage/updated` notification and the
    TUI’s existing handler for that notification.
    
    This change does not alter how token usage is computed, accumulated,
    compacted, or written during turns. It only exposes the token usage that
    resume and fork reconstruction already restored.
    
    This change does not broadcast historical usage replay to every
    subscribed client. The replay is intentionally scoped to the connection
    that requested resume or fork so already-attached clients are not
    surprised by an old usage update while they may be rendering live
    activity.
    
    ## Tradeoffs
    
    Sending the usage notification after the JSON-RPC response preserves a
    clear lifecycle order: the client first receives the thread object, then
    receives restored usage for that thread. The tradeoff is that usage is
    still a notification rather than part of the `thread/resume` or
    `thread/fork` response. That keeps the protocol shape stable and avoids
    duplicating usage fields across response types, but clients must
    continue listening for notifications after receiving the response.
    
    The helper selects the latest non-in-progress turn id for the replayed
    usage notification. This is conservative because restored usage belongs
    to completed persisted accounting, not to newly attached in-flight work.
    The fallback to the last turn preserves a stable wire payload for
    unusual histories, but histories with no meaningful completed turn still
    have a weak attribution story.
    
    ## Architecture
    
    Core already seeds `Session` token state from the last persisted rollout
    `TokenCount` during `InitialHistory::Resumed` and
    `InitialHistory::Forked`. The new core accessor exposes the complete
    `TokenUsageInfo` through `CodexThread` without giving app-server direct
    session mutation authority.
    
    App-server calls that accessor from three lifecycle paths: cold
    `thread/resume`, running-thread resume/rejoin, and `thread/fork`. In
    each path, the server sends the normal response first, then calls a
    shared helper that converts core usage into
    `ThreadTokenUsageUpdatedNotification` and sends it only to the
    requesting connection.
    
    The tests build fake rollouts with a user turn plus a persisted token
    usage event. They then exercise `thread/resume` and `thread/fork`
    without starting another model turn, proving that restored usage arrives
    before any next-turn token event could be produced.
    
    ## Observability
    
    The primary debug path is the app-server JSON-RPC stream. After
    `thread/resume` or `thread/fork`, a client should see the response
    followed by `thread/tokenUsage/updated` when the source rollout includes
    token usage. If the notification is absent, check whether the rollout
    contains an `event_msg` payload of type `token_count`, whether core
    reconstruction seeded `Session::token_usage_info`, and whether the
    connection stayed attached long enough to receive the targeted
    notification.
    
    The notification is sent through the existing
    `OutgoingMessageSender::send_server_notification_to_connections` path,
    so existing app-server tracing around server notifications still
    applies. Because this is a replay, not a model turn event, debugging
    should start at the resume/fork handlers rather than the turn event
    translation in `bespoke_event_handling`.
    
    ## Tests
    
    The focused regression coverage is `cargo test -p codex-app-server
    emits_restored_token_usage`, which covers both resume and fork. The core
    reconstruction guard is `cargo test -p codex-core
    record_initial_history_seeds_token_info_from_rollout`.
    
    Formatting and lint/fix passes were run with `just fmt`, `just fix -p
    codex-core`, and `just fix -p codex-app-server`. Full crate test runs
    surfaced pre-existing unrelated failures in command execution and plugin
    marketplace tests; the new token usage tests passed in focused runs and
    within the app-server suite before the unrelated command execution
    failure.
  • Add codex_hook_run analytics event (#17996)
    # Why
    Add product analytics for hook handler executions so we can understand
    which hooks are running, where they came from, and whether they
    completed, failed, stopped, or blocked work.
    
    # What
    - add the new `codex_hook_run` analytics event and payload plumbing in
    `codex-rs/analytics`
    - emit hook-run analytics from the shared hook completion path in
    `codex-rs/core`
    - classify hook source from the loaded hook path as `system`, `user`,
    `project`, or `unknown`
    
    ```
    {
      "event_type": "codex_hook_run",
      "event_params": {
        "thread_id": "string",
        "turn_id": "string",
        "model_slug": "string",
        "hook_name": "string, // any HookEventName
        "hook_source": "system | user | project | unknown",
        "status": "completed | failed | stopped | blocked"
      }
    }
    ```
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Make thread unsubscribe test deterministic (#18000)
    ## Summary
    - replace the unsubscribe-during-turn test's sleep/polling flow with a
    gated streaming SSE response
    - add request-count notification support to the streaming SSE test
    server so the test can wait for the in-flight Responses request
    deterministically
    
    ## Scope
    - codex-rs/app-server/tests/suite/v2/thread_unsubscribe.rs
    - codex-rs/core/tests/common/streaming_sse.rs
    
    ## Validation
    - Not run locally; this is a narrow extraction from the prior CI-green
    branch.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>