## Why
Standalone image generation remained top-level-only in code-mode
sessions.
## What changed
- Change imagegen exposure from `DirectModelOnly` to `Direct`.
- Keep direct-mode access while enabling nested code-mode access.
- Add a focused regression test for the exposure contract.
## Validation
- `just test -p codex-image-generation-extension`
## Why
`profile_sandbox_mode` was left over from the old selected legacy
profile path. Production now always derives permissions without that
value, and legacy profile contents are ignored, so keeping a parameter
that is always `None` makes `derive_permission_profile` look like it
still supports a fallback that no longer exists.
## What Changed
- Removed the `profile_sandbox_mode` argument from
`ConfigToml::derive_permission_profile`.
- Updated the production caller and legacy sandbox-policy test helper to
match.
- Dropped the stale unselected legacy-profile sandbox test that only
protected the removed fallback shape.
## Verification
- `just test -p codex-config`
- `just test -p codex-core 'config::'`
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/25943).
* #25926
* __->__ #25943
## Stack
1. #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. This PR (#25867) - Add remote request permissions integration
coverage: verifies the selected remote environment across request,
approval, grant reuse, and exec.
This PR is stacked on #25862 and should be reviewed after #25850,
#25858, and #25862.
## Why
The environment-scoped permission stack needs one end-to-end check that
exercises the CCA-shaped path, not only unit-level parsing. This
verifies that a model-sent `environmentId` on `request_permissions`
reaches the approval event, stores the grant under the selected
environment, and is reused by a later tool call in that same
environment.
## What Changed
- Adds a remote executor integration test for `request_permissions` with
`environmentId: remote` and a relative write root.
- Asserts the permission event reports the remote environment and cwd,
and that the normalized grant resolves under the remote cwd.
- Approves the grant, then runs a remote `exec_command` without explicit
per-call permissions and verifies it completes without another exec
approval and writes only in the remote filesystem.
## Verification
- Not run locally per instruction.
- `git diff --check`
## Why
`code_mode_only` moved ordinary runtime tools behind `exec`, but it also
hid hosted Responses tools. Hosted `web_search` and `image_generation`
do not have a nested `exec` runtime path, so code-only sessions lost
those capabilities entirely even when their existing provider, auth,
model, and configuration gates passed.
## What changed
- Keep hosted Responses tools top-level in `code_mode_only` sessions
after their existing gates pass.
- Preserve the existing nested-tool behavior for ordinary runtimes and
the direct-only behavior for multi-agent v2 tools.
- Add planner coverage for `code_mode_only` with default multi-agent v2
settings, hosted live web search, and hosted image generation.
## Verification
- Added focused regression coverage in
`codex-rs/core/src/tools/spec_plan_tests.rs`.
- Left execution to CI per repository workflow.
## Summary
- Splits the monolithic `codex-cloud-config` implementation into focused
modules.
- Keeps behavior unchanged from the preceding config bundle runtime
switch.
## Details
This is the reviewability follow-up after the lineage-preserving
migration PRs. The split separates backend transport, loader
construction, cache handling, metrics, validation, service
orchestration, and focused tests into named files.
Verification: `just fmt`; `just test -p codex-cloud-config`.
## Why
Guardian review turns already submit a read-only `PermissionProfile`,
which is the permissions model the runtime should honor. Passing the
equivalent legacy `SandboxPolicy` through `ThreadSettingsOverrides`
keeps two representations of the same read-only constraint alive on this
path and makes the guardian flow depend on compatibility plumbing that
is being phased out.
## What Changed
- Set `sandbox_policy` to `None` when the guardian review session
submits its child `Op::UserInput`.
- Keep `permission_profile: Some(PermissionProfile::read_only())` and
`approval_policy: Some(AskForApproval::Never)`, so the guardian review
remains read-only and cannot request approvals.
- Remove the now-unused `SandboxPolicy` import and redundant comment
from `codex-rs/core/src/guardian/review_session.rs`.
## Verification
Not run locally; this is a narrow cleanup of redundant thread-settings
override state.
## Summary
- update the app-server image generation integration test to use
`TestAppServer`
- completes the test helper rename from #25701 for this newer test file
## Validation
- `cargo fmt -- --config imports_granularity=Item`
- `cargo check -p codex-app-server --test all`
Note: `just fmt` ran Rust formatting but failed on Python/SDK formatting
because the sandbox could not access the local `uv` cache.
## Summary
- Adapts the moved `codex-cloud-config` crate from the legacy cloud
requirements endpoint to the new config bundle endpoint.
- Switches runtime consumers from `CloudRequirementsLoader` to
`CloudConfigBundleLoader` so one shared bundle supplies cloud-delivered
config and requirements.
- Removes the legacy cloud requirements domain loader path.
## Details
This intentionally keeps `codex-cloud-config` monolithic for review
lineage: the previous PR establishes the crate move, and this PR shows
the behavior change against that moved implementation. A follow-up PR
splits the module back into focused files.
The new bundle path preserves the important cloud requirements loader
semantics where intended: account-scoped signed cache, 30 minute TTL, 5
minute refresh cadence, retry/backoff, auth recovery, and fail-closed
startup loading. The cached payload changes from a single requirements
TOML string to the backend-delivered bundle, and validation rejects
malformed config or requirements fragments before cache write/use.
## Summary
- carry `workspace_kind` from Responses API client metadata into the
turn resolved analytics fact
- serialize the optional value on `codex_turn_event`
- cover both the turn metadata source and turn event serialization
The `workspace_kind` tells us whether a thread had a project attached vs
projectless. this is an indicator for who is adopting Codex for
knowledge work outside of coding
## Testing
- `env UV_CACHE_DIR=/private/tmp/uv-cache
/private/tmp/cargo-tools/bin/just fmt`
- `env PATH=/private/tmp/cargo-tools/bin:$PATH
CARGO_HOME=/private/tmp/cargo-home UV_CACHE_DIR=/private/tmp/uv-cache
/private/tmp/cargo-tools/bin/just test -p codex-analytics`
- `env PATH=/private/tmp/cargo-tools/bin:$PATH
CARGO_HOME=/private/tmp/cargo-home UV_CACHE_DIR=/private/tmp/uv-cache
/private/tmp/cargo-tools/bin/just test -p codex-core turn_metadata`
Paired with openai/openai#970661, which keeps forwarding the same
metadata key through Responses API headers.
## Why
Fixes#24944.
On Windows, app-server resume could reject an active running thread when
the requested session path used normal `C:\...` form and the
already-running path used verbatim `\\?\C:\...` form. The paths point at
the same JSONL file, but the resume stale-path guard compared raw
`PathBuf`s, so desktop resume and heartbeat flows could fail with a
mismatched-path error.
## What Changed
- Compare requested and active rollout paths with
`path_utils::paths_match_after_normalization`.
- Extend the existing running-thread mismatched-path test with a
Windows-only same-file resume case before the stale-path rejection.
## Verification
- `just test -p codex-app-server
thread_resume_rejects_mismatched_path_for_running_thread_id`
## Stack
1. #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. This PR (#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 #25858, and #25867 is stacked on this PR.
## Why
PR2 lets the model bind a `request_permissions` call to a selected
environment, but the approval event and client-facing request still
needed to carry that binding. For CCA, the user-facing prompt and
delegated approval path should know which environment the grant applies
to instead of relying on cwd alone.
## What Changed
- Added optional `environmentId` to `RequestPermissionsEvent`.
- Emit the selected environment id from core permission approval events.
- Preserve the environment id through delegate forwarding, including
cwd-based delegated requests.
- Added `environmentId` to app-server permission approval params,
generated schema/TypeScript artifacts, and README examples.
- Preserve and display the environment id in TUI permission approval
prompts.
- Updated focused core, app-server protocol, and TUI conversion
coverage.
## Testing
Not run locally per instruction. Performed read-only `git diff --check`.
## Why
Standalone image-generation extensions emitted turn items through the
low-level event path, bypassing host-owned finalization such as image
persistence and contributor processing. At the same time, the
generated-image save-path hint must remain visible to the model through
the extension tool's `FunctionCallOutput`, rather than the legacy
built-in developer-message path.
## What changed
- Extended `ExtensionTurnItem` to support image-generation items while
keeping the extension-facing emitter API limited to `emit_started` and
`emit_completed`.
- Routed extension completion through core `finalize_turn_item`, so
standalone image-generation items receive host-owned processing and
persisted `saved_path` values before publication.
- Kept legacy built-in image generation on its existing
developer-message hint path, while standalone image generation returns
its deterministic saved-path hint in `FunctionCallOutput`.
- Shared the image artifact path and output-hint formatting used by core
and the image-generation extension.
- Passed thread identity through extension tool calls so standalone
image generation can construct the same intended artifact path as core.
- Added an app-server integration test covering real standalone image
generation, saved artifact publication, model-visible output hint
wiring, and absence of the legacy developer-message hint.
## Validation
- `just fmt`
- `just test -p codex-image-generation-extension`
- `just test -p codex-web-search-extension`
- `just test -p codex-goal-extension`
- `just test -p codex-memories-extension`
- Targeted `codex-core` tests for image save history, extension
completion finalization, and contributor execution
- `just test -p codex-app-server
standalone_image_generation_returns_saved_path_hint_to_model`
- `just fix -p codex-core`
- `just fix -p codex-image-generation-extension`
- `just bazel-lock-update`
- `just bazel-lock-check`
## 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.
## 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
## 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
## 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.
## 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`.
## 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`
## 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
## 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.
## 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`
## 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.
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.
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.
## 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)
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.
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.
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.
## 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`
## 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`
## 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
## 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.
## 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.
## 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.
## 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`
## 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.
## 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
## 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>
## Why
Fixesopenai/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`
## 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.
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`.
## 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.
## 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`
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.)
## 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`