## Why
This PR keeps the mechanical helper extraction separate from the
behavior change in #29665. The follow-up needs the token-window
accounting from `turn.rs` in another call path, but reviewing that is
much easier when the helper extraction is separate from the semantic
change.
## What
- Adds `session/context_window.rs` with `ContextWindowTokenStatus`.
- Moves the existing auto-compaction token-status calculation out of
`session/turn.rs`.
- Replaces the duplicated inline remaining-token calculation in
`turn.rs` with `tokens_until_compaction()`.
This PR is intended to be behavior-preserving. The
`get_context_remaining` behavior change is stacked separately in #29665.
## Testing
- `just test -p codex-core auto_compact_body_after_prefix`
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/29664).
* #29665
* __->__ #29664
## Summary
- surface shared rollout-budget exhaustion as
`CodexErr::RolloutBudgetExceeded` instead of a generic interrupted turn
- map it through the existing `CodexErrorInfo` and app-server v2
`codexErrorInfo` path
- keep local compaction from retrying after the shared rollout budget is
exhausted
This gives app-server clients a stable `rolloutBudgetExceeded` error
they can classify without guessing from `status="interrupted"`.
## Tests
- `just test -p codex-core rollout_budget`
## Why
Selected capability roots belong to the executor filesystem, not the
app-server host. Converting their path strings into the host's native
`Path` breaks whenever the two machines use different path conventions,
such as a Windows executor behind a Unix app-server.
This PR establishes `PathUri` as the selected-plugin boundary so the
executor remains authoritative for its paths.
## What changed
- Require `selectedCapabilityRoots[].location.path` to be a canonical
`file:` URI and deserialize it directly as `PathUri`; native path
strings are rejected.
- Update the app-server schema, generated TypeScript, examples, and
request coverage for the URI contract.
- Keep selected roots, resolved plugin locations, manifest paths, and
manifest resources as `PathUri`.
- Inspect and read plugin roots and manifests only through the selected
environment's `ExecutorFileSystem`.
- Parse executor manifests with the shared URI-native parser from #29620
instead of projecting them onto the host filesystem.
- Enforce resource containment lexically and preserve the root URI's
POSIX or Windows path convention.
- Cover foreign Windows plugin roots and URI-native manifest resources.
```text
thread/start
selectedCapabilityRoots[].location.path = "file:///C:/plugins/demo"
| PathUri
v
ExecutorFileSystem
|
+--> plugin.json
+--> manifest resources
```
This PR stops at the shared selected-plugin representation. The next two
PRs remove the remaining host-path projections in the skill and MCP
consumers.
## Stack
1. #29614 — add lexical `PathUri` containment.
2. #29620 — share URI-native manifest path resolution.
3. **This PR** — keep selected plugin roots and resources URI-native.
4. #29626 — load executor skills without host path conversion.
5. #29628 — resolve executor MCP working directories without host path
conversion.
## Why
PR #29494 made context-window IDs visible to the model by wrapping the
token-budget window payload in `<context_window>`, but rollout JSONL
consumers still could not see the initial window identity by tailing the
session file. Compacted rollout items carry window IDs only after
compaction has happened, so a session with no compaction had no durable
JSONL record for window 0.
This change gives tailing consumers a stable initial-window record at
session creation time.
## What Changed
- Added `session_meta.context_window.window_id` for the initial
context-window identity.
- `CreateThreadParams` now requires `initial_window_id: String`, so
thread-store callers cannot accidentally create new threads without
window-0 metadata.
- Live thread creation derives the persisted initial window ID from the
same `AutoCompactWindowIds` used to initialize `SessionState`, keeping
runtime state and JSONL metadata aligned.
- Rollout reconstruction uses `session_meta.context_window.window_id` as
the initial-window fallback and derives `window_number = 0`,
`first_window_id = window_id`, and `previous_window_id = None`
internally.
- Fork reconstruction intentionally uses the same rollout reconstruction
path; consumers that need to distinguish copied initial-window metadata
can use the rollout `thread_id`.
- Legacy compactions without `window_number` still use compaction-count
fallback accounting instead of being reset to window 0 by the
initial-window fallback.
- Compacted rollout metadata still takes precedence once compaction
records exist, preserving the richer chain fields there.
## JSONL Shape
Real rollout JSONL is one object per line. This example is expanded for
readability, but shows the new initial `session_meta.context_window`
record followed by the existing compacted rollout item shape that also
carries window IDs:
```jsonl
{
"timestamp": "2026-06-22T12:00:00.000Z",
"type": "session_meta",
"payload": {
"session_id": "<THREAD_ID>",
"id": "<THREAD_ID>",
"timestamp": "2026-06-22T12:00:00.000Z",
"cwd": "/repo",
"originator": "codex",
"cli_version": "0.0.0",
"source": "cli",
"model_provider": "<MODEL_PROVIDER>",
"context_window": {
"window_id": "<INITIAL_WINDOW_ID>"
}
}
}
...
{
"timestamp": "2026-06-22T12:34:56.000Z",
"type": "compacted",
"payload": {
"message": "<COMPACTION_SUMMARY>",
"replacement_history": [
"..."
],
"window_number": 1,
"first_window_id": "<INITIAL_WINDOW_ID>",
"previous_window_id": "<INITIAL_WINDOW_ID>",
"window_id": "<NEXT_WINDOW_ID>"
}
}
```
The nested `context_window` object is intentional: it gives rollout
consumers a stable namespace for context-window metadata while only
writing the non-derivable initial `window_id`. For the initial window,
`window_number`, `first_window_id`, and `previous_window_id` are derived
internally instead of being written to the rollout.
## Verification
- `just test -p codex-protocol`
- `just test -p codex-rollout
recorder_materializes_on_flush_with_pending_items`
- `just test -p codex-core reconstruct_history`
- `just test -p codex-core
record_initial_history_reconstructs_forked_transcript`
- `just test -p codex-thread-store`
- `just test -p codex-state`
- `just test -p codex-app-server
thread_read_returns_summary_without_turns`
- `just test -p codex-rollout persistence_metrics`
## Why
Use a clearer name for what happens when this helper sets up a test
environment.
## What
- Rename the builder and its harness wrapper to use `auto_env` instead
of `remote_env` because the helper will set up a local environment if
configured by the build system.
## Why
Core tests should branch on the executor's operating system, not on
runner details such as Docker or Wine. This keeps platform behavior
stable as new test backends are added and reserves Wine-specific skips
for actual runner debt.
## What
- Add `TestTargetOs` and target/host-aware skip helpers while keeping
`TestEnvironment` internal.
- Replace topology enum access with remote predicates and a narrow
Docker accessor.
- Migrate OS-semantic Wine skips, preserve runner-specific gaps, and
document the skip taxonomy.
## Validation
- `just test -p core_test_support`
- `just test -p codex-core
remote_test_env_can_connect_and_use_filesystem`
- `bazel test //codex-rs/core:core-all-wine-exec-test
--test_output=errors` reached test execution; unrelated existing
view-image, path, and timing failures remain.
- `just test -p codex-core` and `just test` reached broad test
execution; this checkout has unrelated helper, sandbox, and timing
failures.
Mechanical rename of CodeModeService => InProcessCodeModeSession
This already implements a CodeModeSession as its prime interface to
Core. The name was vestigial _and_ confusing af when embedded inside
core::tools::code_mode::CodeModeService
## Why
auto review reviews Codex App tool calls using connector metadata such
as the app ID, name, and description. That metadata does not identify
the account behind the OAuth connection.
For Google Drive, this means auto review cannot distinguish a Drive
connection authenticated as `user@email.com` from a personal Drive
account. Uploading work data can therefore look like a transfer to a
personal destination even though the connector service already knows the
authenticated account email.
## What changed
- Read `_meta._codex_apps.connected_account_email` while resolving
approval metadata for built-in Codex App tools.
- Include the connected account email in the structured MCP tool action
sent to auto review.
- Trim empty values and omit the field when the connector link has no
account email.
- Update existing auto review request constructors and add coverage for
request construction and JSON serialization.
## Security
Only metadata from the trusted built-in `codex_apps` MCP server is
accepted. Custom MCP servers cannot inject a connected account email
into auto review reviews; the new regression test verifies that spoofed
metadata is ignored.
The email is used only in auto review's private review request. This
change does not add it to model-visible tool descriptions, app-server
approval events, or auto review assessment/review analytics.
[Codex Thread
019edc37-5345-7272-92c9-bf5494cf3819](https://codex-thread-link.openai.chatgpt-team.site/thread/019edc37-5345-7272-92c9-bf5494cf3819)
## Summary
- count MCP `CallToolResult.isError` responses as failed calls instead
of successful transport-level calls
- add `codex.mcp.call.error` with bounded `error_type` and trusted
plugin-service `error_code` dimensions
- record the same error classification on MCP tool-call spans while
keeping untrusted server error text out of metric labels
## Scope
- no changes to MCP routing, retries, tool behavior, configuration, or
public APIs
- request failures remain grouped as `mcp_request`; separating
connection, timeout, protocol, and JSON-RPC failures requires preserving
typed errors through the existing flattened error boundary
## Testing
- `just test -p codex-core 'mcp_tool_call::tests::'` (75 passed)
- `just fix -p codex-core`
- `just fmt`
- `just test -p codex-core` (2,676 passed; 80 unrelated environment
failures from missing test binaries, sandbox signals, and read-only
paths)
## Why
With deferred executors, an environment can become ready between two
sampling requests in the same turn. The model-visible environment
update, advertised tools, and eventual tool execution must all describe
the same request-time view.
Otherwise, a request built while only environment B is ready can
advertise a tool without an `environment_id`; if higher-priority
environment A becomes ready before execution, that call could silently
run in A instead.
This PR is stacked on #29527.
## Design
`run_turn` captures one `Arc<StepContext>` at each sampling-request
boundary. That step owns the request's `TurnContext` and environment
snapshot.
- World-state environment updates and tool planning borrow that same
step.
- `ToolCallRuntime` retains the `Arc` while asynchronous tool calls
execute.
- `ToolInvocation` carries the step to handlers; its temporary `turn`
compatibility field is derived from the same object.
- `ToolRouter` does not retain `StepContext`; it only uses it while
constructing the request's tool set.
- With `DeferredExecutor` disabled, step capture keeps using the
environments frozen at turn start.
Simply: every sampling request gets one consistent picture of its
environments, from what the model sees through where its tool calls run.
## What changed
- Build environment-dependent tool specs from the current request's
`StepContext`.
- Use that same step for unified exec, legacy shell, `apply_patch`,
`view_image`, and `request_permissions` execution.
- Hide environment-backed tools, including `request_permissions`, while
no environment is attached.
- Resolve legacy shell paths and metadata from the selected step
environment instead of the stale turn-start environment.
- Capture explicit steps at non-turn-loop boundaries such as compaction,
prompt debug, and startup prewarm.
- Reconcile prompt-debug history from the same step used to build its
tools.
## Follow-up
- Bind yielded code-mode cells to the tool runtime that created them, so
nested calls made after yielding continue to use the originating
request's `StepContext`.
## Test plan
- `just test -p codex-core
deferred_executor_updates_context_and_tools_after_startup`
- `just test -p codex-core
environment_count_controls_environment_backed_tools`
- `just test -p codex-core
build_prompt_input_includes_context_and_user_message`
## Why
view_image needs to support foreign OS remote executors.
## What
- resolve image paths against the selected environment as `PathUri` and
read them through that environment's filesystem
- keep app-server's public path field wire-compatible as
`LegacyAppPathString`, with purpose-specific UI rendering
- cover relative and absolute target-native paths in the core
integration test and run the full `view_image` suite under wine-exec
without skips
## Summary
- allow the native Responses API `image_generation` tool when the active
provider carries CCA's non-empty `x-openai-actor-authorization` header
- preserve the Codex-managed ChatGPT auth path, scoped to providers that
actually require OpenAI auth
- keep generic custom providers excluded, including when unrelated
ChatGPT credentials are cached
- retain the existing feature, provider-capability, and
image-input-modality gates
## Why
CCA authenticates its inference requests through the active provider's
`x-openai-actor-authorization` and `ChatGPT-Account-ID` headers, so it
does not have a Codex-managed login session. The previous gate therefore
hid the native hosted image-generation tool despite an authenticated
codex-backend path.
This change is intentionally limited to the native hosted tool. It adds
no extension, MCP, plugin-service, session-source, token plumbing, or
new provider configuration surface.
## Tests
- `cargo test -p codex-core
hosted_tools_follow_provider_auth_model_and_config_gates`
- `cargo fmt --all -- --check`
- `git diff --check origin/main`
## Why
Managed network configures commands to use local HTTP and SOCKS proxies.
For commands delegated to the exec server, the proxy environment and the
sandbox policy were prepared separately. On macOS, that meant a command
could receive `HTTPS_PROXY=http://127.0.0.1:43123` while Seatbelt still
denied access to port `43123`.
## What changed
`NetworkProxy` now prepares the command environment and sandbox context
together from the same runtime snapshot:
```text
Prepared managed network
├── command environment: HTTPS_PROXY=http://127.0.0.1:43123
└── sandbox context: allow outbound to 127.0.0.1:43123
```
That context travels with remote exec requests. The exec server
preserves the managed proxy and CA environment, and macOS Seatbelt
allows only the prepared loopback proxy ports without enabling broad
network access or local binding.
The protocol field is optional and the existing enforcement flag remains
in place, preserving compatibility with callers that do not send the new
context.
## Why
Follow-up to #29249 and its [compaction review
thread](https://github.com/openai/codex/pull/29249#discussion_r3455055101).
During a turn, environment readiness can change between sampling
requests. Inline compaction must render the same model-visible
`WorldState` used by the request it follows. Rebuilding that state
during compaction can observe a newer environment, make replacement
history disagree with what the model saw, and suppress the next
environment update.
## What changed
- Make `run_turn` own the current `Arc<WorldState>` and replace it only
between sampling requests.
- Build each state from an explicitly chosen environment snapshot, diff
deferred-executor steps against the turn-owned state, and retain the
latest state in `ContextManager` only for cross-turn and resume
tracking.
- Pass the exact turn-owned state into inline compaction and explicit
new-context-window replacement.
- Carry that state with
`InitialContextInjection::BeforeLastUserMessage`, so replacement context
and its stored baseline cannot come from different snapshots.
- Remove obsolete state-recapture helpers and ambiguous TurnContext-only
WorldState builders.
- Add an integration test that moves an environment from starting to
ready during a paused turn, triggers compaction, and verifies the next
request receives the readiness update exactly once.
## Test plan
- `just test -p codex-core
deferred_executor_compaction_preserves_then_updates_environment_once`
- `just test -p codex-core process_compacted_history`
- `just test -p codex-core mid_turn_continuation_compaction`
- `just test -p codex-core build_initial_context`
- `just test -p codex-core
ignores_session_prefix_messages_when_truncating`
## Summary
MCP refresh replaced the published connection manager without shutting
down the manager it superseded. If another task retained that old
manager, its stdio MCP processes stayed alive and accumulated across
refreshes.
Atomically swap in the refreshed manager, then explicitly shut down the
exact manager returned by the swap. Add a process-level regression test
that retains the old manager during refresh and verifies its stdio
process exits while the replacement remains available.
## Context
Explicit cleanup was lost when manager publication moved to `ArcSwap`.
Dropping the old manager is not a reliable shutdown boundary because
active callers can retain its `Arc` and underlying client process
handles.
## Summary
- rename `reminder_interval_model_requests` to
`reminder_interval_seconds`
- read the configured time provider before every model request and
inject a reminder only after the configured number of seconds has
elapsed
- preserve immediate first delivery and forced delivery after compaction
changes the context window
## Tests
- `just test -p codex-core current_time_reminder`
## Summary
Resuming a persisted thread currently deep-clones its complete rollout
history several times. `InitialHistory` is retained for the app-server
response, copied into thread persistence, and copied again by read-only
accessors. These copies scale with the complete rollout rather than the
bounded model context and add measurable latency for large sessions.
This change stores resumed rollout history in `Arc<Vec<RolloutItem>>`.
Rollout loading wraps the parsed vector once, while app-server response
construction, session initialization, and thread persistence share it
through inexpensive `Arc` clones. Read-only history access now returns a
borrowed slice, and fork paths use `Arc::unwrap_or_clone` where they
genuinely need mutable ownership. Rollout reconstruction also consumes
its temporary context instead of cloning the reconstructed model
history.
The serialized representation remains unchanged. In an artificial 123 MB
rollout benchmark, sharing resumed history reduced cold resume latency
by roughly 9–10%. The affected crates compile with their test targets,
all 80 thread-store tests pass, and the Bazel dependency lock remains
valid.
## Summary
Multi-agent v2 tools now use the fixed `collaboration` namespace when
namespace tools are available. This keeps the model-visible hint and the
actual tool surface aligned around `functions.collaboration.*`, without
exposing an unshipped namespace knob to users.
The PR also removes the old `features.multi_agent_v2.tool_namespace`
config/schema surface, updates the MAv2 test fixtures for namespaced
calls, and fixes stale `TurnContext.features` references that were
breaking `codex-core` builds.
## Changes
- Expose MAv2 tools under `collaboration` instead of relying on a
configurable namespace.
- Remove `tool_namespace` from MAv2 TOML config, resolved config,
validation, schema, and tests.
- Update tool-planning and integration fixtures to assert or emit
namespaced MAv2 tool calls.
- Read feature state through `TurnContext.config.features` in the
multi-agent mode context paths.
## Testing
- `just write-config-schema`
- `just test -p codex-features`
## Summary
- Require the reserved Codex Apps MCP server name to be present in the
connection manager before treating it as host-owned.
- Update auth elicitation tests to model an installed host-owned Codex
Apps server without sending startup events to the test session.
## Why
PR #29518 replaced the old host-owned flag with a name-only check. That
made non-host-owned tests with the reserved codex_apps name enter auth
elicitation and wait forever for a response.
## Summary
- let `codex sandbox` accept the JSON value from
`codex/sandbox-state-meta`
- require the payload `permissionProfile` instead of falling back to
ambient permissions
- reuse the existing macOS, Linux, and Windows launch paths, treating
external sandbox state conservatively as read-only
- let opaque forwarders add runtime read roots and disable direct
network access without decoding the payload
Builds on #29113, which is now on `main`.
## Tests
- `just test -p codex-cli debug_sandbox::tests`
- `cargo build -p codex-rmcp-client --bin test_stdio_server`
- `just test -p codex-core
stdio_mcp_tool_call_includes_sandbox_state_meta`
- `just test -p codex-mcp`
- `just fmt`
## Why
Codex Apps-specific behavior is currently distributed across cache
helpers, startup, tool conversion, and model-visible annotation. Each
layer independently checks the reserved server name, which obscures the
boundary between trusted host-owned connector metadata and regular MCP
server data.
Classifying the server once when `AsyncManagedClient` is created gives
the client a single source of truth and makes the two processing paths
explicit.
## What changed
- Record whether an `AsyncManagedClient` represents the Codex Apps
server at construction time.
- Route startup cache loading, cache persistence, and cache telemetry
through the Codex Apps branch.
- Split uncached tool conversion between Codex Apps normalization and
regular MCP metadata sanitization.
- Split model-visible schema and plugin provenance handling along the
same boundary.
- Remove redundant server-name guards from helpers that are now called
only from the Codex Apps branch.
## Verification
- Preserve behavioral coverage that verifies Codex Apps connector
metadata and the complete converted `ToolInfo` shape.
## Stack
Depends on #29518.
When using Responses Lite, we should all use `additional_tools` and a
developer item instead of the top level tools array & instructions
field. This keeps things 1-to-1.
Forced namespacing for _all_ tools will land in a following PR after
some coordination & fixes in Responses API (around collisions & return
items).
The goal is to eventually expand the scope of this to _all_ requests
from codex, but that will require larger coordination across providers &
slower rollout.
## Why
Codex Apps server admission is already decided before
`McpConnectionManager` is constructed. `effective_mcp_servers` and
`effective_mcp_servers_from_configured` remove the server when the apps
feature or required authentication is unavailable, so storing the same
decision on the manager duplicates state that can drift from the
effective server map.
## What changed
- Remove `host_owned_codex_apps_enabled` from `McpConnectionManager` and
its constructor.
- Identify the host-owned Codex Apps server by its reserved server name
once it is present in the effective server map.
- Remove the now-unused flag calculations and constructor arguments from
production and test callsites.
## Summary
- express remote compaction result handling as an exhaustive match
- preserve the special `TurnAborted` path without emitting a generic
compaction error
- rely on the standard `test_codex` provider setup in the compaction
budget test
Follow-up to review feedback on #28707.
## Testing
- `just test -p codex-core
compaction_budget_exhaustion_aborts_without_error_or_retry`
- `just fmt`
## Summary
NVIDIA asked to measure Fast mode usage and reasoning effort from Codex
CLI OTEL logs. Add the finalized `service_tier` and
`model_reasoning_effort` to the existing `codex.sse_event`
`response.completed` record.
This intentionally reuses the existing completion event and leaves
transport APIs and shared telemetry plumbing unchanged.
## Testing
- `cargo build -p codex-cli --bin codex`
- `just test -p codex-core responses_api_emits_api_request_event`
- End-to-end with the built CLI and a local OTLP/HTTP collector:
- Fast/high emitted `service_tier=priority` and
`model_reasoning_effort=high` with token usage.
- Standard/low omitted `service_tier` and emitted
`model_reasoning_effort=low` with token usage.
## Summary
- read the request-scoped safety-buffering treatment from HTTP response
headers and per-turn WebSocket metadata through one shared header parser
- combine that treatment with Responses API safety-buffering signals
- propagate `showBufferingUi` and nullable `fasterModel` through the
existing `model/safetyBuffering/updated` app-server notification
- update the app-server documentation and generated JSON and TypeScript
schemas
The public implementation contains no model mapping or real model
identifier. Tests and protocol examples use generic `current-model` and
`faster-model` placeholders only.
## Dependencies
- server-side treatment evaluation:
https://github.com/openai/openai/pull/1060247
- initial Responses API safety-buffering propagation:
https://github.com/openai/codex/pull/29371
- Codex App UI: https://github.com/openai/openai/pull/1057789
## Validation
- Codex API tests: 129 passed
- focused Codex core safety-buffering integration test passed
- app-server protocol tests passed after regenerating schema fixtures
- Clippy fix and repository formatting completed successfully
The broader app-server run compiled all changed crates and completed
with 1,269 passing tests. Its remaining failures were unrelated
environment limitations: macOS sandbox application was denied, one
expected test binary was unavailable, and several existing subprocess
tests timed out as a result.
## Why
Remote stdio MCP servers can run in an environment whose path convention
differs from the Codex host. A Windows cwd such as
`C:\Users\openai\share` is absolute for the executor but was rejected by
a POSIX orchestrator.
Built on #29501, now merged, which only clarifies the host-native
`PathUri` constructor name.
## What changed
- Deserialize MCP cwd values as `LegacyAppPathString` so config does not
apply host path rules.
- Interpret that spelling as host-native for local launches and convert
it to `PathUri` at executor launch.
- Skip host filesystem and command resolution checks for remote stdio in
`codex doctor`.
- Add host-independent config and executor-boundary coverage using the
foreign path convention for each test platform.
## Validation
- `just test -p codex-utils-path-uri -p codex-config -p codex-mcp -p
codex-rmcp-client` (408 passed)
- `just test -p codex-cli -p codex-rmcp-client` (372 passed)
- `cargo check --workspace --tests`
- `just test` (11,311 passed; 43 unrelated environment/timing failures)
- `just fix -p codex-cli -p codex-config -p codex-core -p codex-mcp -p
codex-mcp-extension -p codex-rmcp-client -p codex-tui`
## Why
Amazon Bedrock returns a `401 Unauthorized` response containing
`Signature expired:` when an AWS credential, including a short-lived
`AWS_BEARER_TOKEN_BEDROCK`, has expired. Codex currently surfaces that
response as a generic `unexpected status` error, which does not explain
how to recover.
Environment-provided bearer tokens cannot be refreshed automatically, so
the error should direct users to refresh their AWS credentials or
replace or remove the environment token and restart Codex. This
classification belongs to the Amazon Bedrock provider so similar
responses from other providers retain their existing behavior.
## What changed
- Add a synchronous `ModelProvider::map_api_error` hook that defaults to
the existing provider-neutral API error mapping, and route model
request, stream, WebSocket, and terminal unauthorized errors through the
active provider.
- Override the hook for Amazon Bedrock. After preserving the structured
status, body, URL, and request metadata, recognize `401` responses
containing `Signature expired:` and attach actionable credential
guidance.
- Keep `codex-protocol` provider-neutral by representing the guidance as
an optional `user_message`. Error rendering prefers this message while
continuing to append the URL, request ID, Cloudflare ray, and
authorization diagnostics.
- Add model-provider coverage for expired signatures and negative cases,
core coverage for provider dispatch after unauthorized recovery, and a
TUI snapshot for the rendered error.
## Testing
Tested with a real request with expired bedrock key:
<img width="962" height="126" alt="Screenshot 2026-06-22 at 3 56 51 PM"
src="https://github.com/user-attachments/assets/7e21cc7c-798e-4662-8467-7f304a2f2b59"
/>
seems to be a merge conflict on main:
> pakrym-oai introduced the stale initializer in commit 3b32d861c5, PR
#29249.
> Context: Owen Lin renamed metadata to
internal_chat_message_metadata_passthrough in PR #28968. PR #29249 then
landed afterward with the old field name, causing the compile/Clippy
failure.
## Why
Downstream refactors are producing confusing code with this
functionality having a very generic name. Encoding the specific
conversion approach in the method name makes it clearer.
## What
Rename `PathUri::from_path` to `PathUri::from_host_native_path` and
update its Rust call sites.
## Why
MCP tools were only placed behind `tool_search` when a feature flag was
enabled or when there were at least 100 tools. That made the model's
tool flow depend on both rollout configuration and the number of
installed tools.
The searched-tool flow is now the intended behavior. Making it
unconditional when the model and provider support it gives every
supported setup the same behavior and lets us retire the feature flag
safely.
## What changed
- Defer all effective MCP tools when `tool_search` and namespaced tools
are supported.
- Keep exposing MCP tools directly when search cannot be used, so older
or unsupported model/provider combinations still work.
- Mark `tool_search_always_defer_mcp_tools` as removed and ignore old
configured values.
- Keep plugin filtering, app-only filtering, file handling, and MCP
calls working through the searched-tool flow.
## Why many tests changed
Many tests used to act as if the model could see MCP tools in its first
request and call them immediately. That is no longer the real flow: the
model first receives `tool_search`, searches for a tool, receives the
matching MCP tool, and then calls it in the next request.
The tests therefore needed an extra search step, and checks for tool
names, descriptions, and input fields had to move from the first request
to the search result. These are not separate product changes; they make
the tests follow what the model will actually see after this change. The
plugin tests still check which tools are allowed and where they came
from, the file tests still check upload fields and behavior, and the MCP
round-trip test still checks a successful call from start to finish.
## Tests
- `just test -p codex-features`
- Focused `codex-core` tests for MCP exposure and tool planning
- `just test -p codex-core explicit_plugin_mentions`
- `just test -p codex-core stdio_server_round_trip`
- Focused `codex-core` tests for tool search, app-only tools, and MCP
file uploads
## Description
This PR is a followup to https://github.com/openai/codex/pull/28355 and
starts assigning `internal_chat_message_metadata_passthrough.turn_id` to
durable Responses API items created during a turn.
The goal is that those items keep the `turn_id` that introduced them
when Codex resends stateless HTTP context, reconstructs history for
resume/fork paths, or reuses websocket response state.
## What changed
- Set `internal_chat_message_metadata_passthrough.turn_id` when missing
as response items enter durable history, initial/replacement history,
inter-agent communication history, and local compaction summaries.
- Preserve existing item turn IDs instead of overwriting them during
persistence, resume reconstruction, compaction, forked history, and
websocket incremental reuse.
- Keep `compaction_trigger` fieldless because it is a request control,
not a durable response item.
- Update focused history/request assertions and fixtures for stateless
requests, websocket incrementals, compaction, thread injection, prompt
debug, and related CI coverage.
## What
This PR will extend the existing centralized image-preparation path to
replace HTTP(S) image inputs with a model visible error message. It
won't "ruin" and break existing rollouts, but it will deprecate support
for the pathway. App server clients should no longer use HTTP image urls
if they'd like to upgrade.
The HTTP image url pathway is currently resolved in the responsesapi. It
is slow and not reccomended.
## Behavior
- HTTP(S) image URL: replace with `input_text`
- data URL: use the existing decode and resize path
- other image URL schemes: leave unchanged
This intentionally does not change app-server ingress. That validation
remains a follow-up.
## Test plan
- `just test -p codex-core -E
'test(/image_preparation|prepares_image_failures_before_history_insertion|prepares_resumed_history_before_installing_it|responses_lite_prepares_images/)'`
— 7 passed
- `just fix -p codex-core`
- `just fmt`
Token-budget initial context carries thread and context-window lineage
that the model should treat as one structured context-window block.
Wrapping it in `<context_window>` makes that boundary explicit while
preserving the existing window id content.
Before this change, the window identifiers were injected as an untagged
developer text fragment:
```text
Thread id <THREAD_ID>.
First context window id: <FIRST_WINDOW_ID>
Current context window id: <WINDOW_ID>
Previous context window id: <PREVIOUS_WINDOW_ID>
```
After this change, the same payload is wrapped as a context-window
block:
```text
<context_window>
Thread id: <THREAD_ID>
First context window id: <FIRST_WINDOW_ID>
Current context window id: <WINDOW_ID>
Previous context window id: <PREVIOUS_WINDOW_ID>
</context_window>
```
This adds shared `CONTEXT_WINDOW_*_TAG` protocol constants, updates
`TokenBudgetContext` to render with those markers, treats the new
wrapper as contextual developer content when mapping history, and
refreshes the token-budget request-shape assertions and snapshot.
Verification:
- `just test -p codex-core token_budget`
- `just test -p codex-core
recognizes_context_window_as_contextual_developer_content`
## Why
Environment context is model-visible state, but it is currently
assembled from transient turn values and diffed through
environment-specific paths. That makes initial injection, turn-to-turn
updates, and changes that happen within a turn use different baselines.
This PR introduces the smallest useful model world-state slice:
environments only, with one in-memory baseline and one renderer for full
state and diffs.
## What changed
- Add a typed `WorldState` container whose sections render fragments
relative to an optional previous value. Full rendering uses the same
diff path with no previous state.
- Replace the parallel `EnvironmentContext` representation with an
`EnvironmentsState` section keyed by environment ID and rendered in
deterministic order.
- Preserve the legacy single-environment output while supporting
multiple environments, starting environments, unavailable tombstones,
and changes to persisted turn-context values.
- Store the latest complete `WorldState` on `ContextManager` and use it
for both turn-boundary and mid-turn environment diffs.
- Build initial and post-compaction context from the same world-state
builder, then retain the rendered state as the next baseline.
- Seed the in-memory baseline from the latest `TurnContextItem` when
resuming an existing rollout; the world state itself is not serialized.
- Keep non-world settings updates on their existing path and merge
rendered world-state fragments at the session consumer.
## Known limitation
A legacy `TurnContextItem` only reconstructs the primary environment as
`local`; it cannot faithfully recover a remote-primary environment ID
after resume. Live state uses the exact environment IDs once a complete
baseline is established.
## Test plan
- `just test -p codex-core world_state`
- `just test -p codex-core record_context_updates`
- `just test -p codex-core deferred_executor_`
- `just test -p codex-core build_initial_context`
- `just test -p codex-core rollout_reconstruction`
- `just test -p codex-core
process_compacted_history_reinjects_full_initial_context`
## Summary
- rename `Config::permission_profile_allowed` to
`is_permission_profile_allowed`
- use `BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS` in the TUI and
its assertion
- follow up on the late review comments from #26678
The previous `:danger-no-sandbox` value was an invalid built-in profile
ID. #26678 corrected it to `:danger-full-access`; this PR centralizes
the value to prevent future drift.
## Testing
- Not run per request; `cargo fmt` only
Co-authored-by: Codex <noreply@openai.com>
## Why
`permissionProfile/list` currently advertises every built-in and
configured profile even when effective enterprise requirements prevent
selecting it. That forces each client to reconstruct policy from
lower-level requirement fields, which is easy to miss and difficult to
keep consistent.
The catalog should remain complete so clients can explain that an option
was disabled by an administrator, while also reporting whether each
profile is selectable.
## What
- Add an `allowed` field to each permission profile summary.
- Build a shared catalog from the effective config and current
requirements, including `allowed_sandbox_modes`, `allowed_permissions`,
and filesystem restrictions.
- Use the shared catalog in app-server and the TUI so disallowed
profiles remain visible but cannot be selected.
- Use the canonical `:danger-full-access` profile ID in the TUI.
- Update the app-server schemas, API documentation, behavioral tests,
and TUI snapshots.
## Scope
This PR targets `main` directly and is independent of #24852. It
preserves the current behavior where built-in profiles are constrained
by sandbox-mode requirements and `allowed_permissions` applies to
configured profiles.
## Testing
- `just test -p codex-core
permission_profile_catalog_marks_profiles_disallowed_by_requirements`
- `just test -p codex-app-server permission_profile_list`
- `just test -p codex-app-server-protocol`
- `just test -p codex-tui profile_permissions`
- `just fix -p codex-core`
- `just fix -p codex-app-server-protocol`
- `just fix -p codex-app-server`
- `just fix -p codex-tui`
- `just fmt`
---------
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Joey Trasatti <joey.trasatti@openai.com>
## Summary
Stacked on #26706.
Adds the shared auth/system-proxy contract that later platform resolver
PRs plug into. This PR moves Codex-owned auth and startup HTTP clients
through a common route-aware boundary, but does not yet add Windows or
macOS system proxy resolution.
The default path remains unchanged when `respect_system_proxy` is absent
or disabled.
## Implementation
- Adds `codex-client/src/outbound_proxy.rs` with the shared
route-selection model:
- `OutboundProxyConfig`;
- `ClientRouteClass`;
- `RouteFailureClass`;
- `build_reqwest_client_for_route`.
- Preserves the existing reqwest/default-client behavior when no route
config is supplied.
- Uses the fixed MVP routing policy when route config is supplied:
platform system/PAC/WPAD discovery, then explicit env proxy variables,
then direct connection.
- Keeps platform-specific system discovery behind the shared client
boundary. This PR provides the contract and fallback behavior; later
resolver PRs plug in Windows and macOS discovery.
- Adds `login::AuthRouteConfig` so auth call sites depend on a small
policy type instead of platform resolver details.
- Maps the resolved `Config.respect_system_proxy` boolean into
`AuthRouteConfig` for auth-owned clients.
- Wires the route config through browser login, device-code login,
access-token login, login status, logout/revoke, token refresh, API-key
exchange, app-server account login, TUI/app startup, cloud-config
bootstrap, cloud tasks, plugin auth, and exec startup config loading.
## End-user behavior
- No behavior changes by default.
- When `respect_system_proxy = true`, auth-owned clients opt into the
shared route-aware client path.
- On platforms without a resolver implementation in this PR, system
discovery is unavailable and the route-aware path falls back to explicit
env proxy handling, then direct connection.
- Custom CA handling remains separate from proxy route selection and
still runs through the shared client builder.
- No proxy URLs, PAC contents, or resolved platform details are exposed
through the public config surface introduced here.
## Tests
Adds or updates coverage for:
- preserving default auth-client fallback behavior when no route config
is provided;
- injected environment-proxy fallback without mutating process
environment;
- existing login-server E2E flows using explicit `auth_route_config:
None` to guard unchanged default behavior;
- updated auth manager, login, logout, cloud-config, startup, and
plugin-auth call sites passing route config explicitly.
## Why
`compile_scoped_filesystem_pattern()` accepted a `_policy_cwd` parameter
even though scoped glob compilation no longer uses the policy working
directory. Keeping that unused argument forced the surrounding
permissions compilation path to keep forwarding `policy_cwd` through
call sites that did not need it, making the API look more dependent on
cwd resolution than it is.
## What changed
Removed the unused cwd parameter from
`compile_scoped_filesystem_pattern()` and the callers that only
forwarded it: `compile_filesystem_permission()`,
`compile_permission_profile()`, and
`compile_permission_profile_selection()`. Workspace root resolution
still keeps `policy_cwd`, because that path still resolves relative
roots against the active policy cwd.
Relevant code:
[`codex-rs/core/src/config/permissions.rs`](https://github.com/openai/codex/blob/b8b9816102e064dae4488ec130cf560f63c1ab78/codex-rs/core/src/config/permissions.rs#L346).
## Verification
- `just test -p codex-core config::permissions`
- `just test -p codex-core` was also run after building
`test_stdio_server`; it passed the touched permissions coverage but
still reported unrelated existing failures in `cli_stream` and shell
snapshot tests.
## Why
The first auto-review currently creates its Guardian child session on
demand, adding avoidable latency before the review can begin. Creating
the ordinary Guardian child during parent-session initialization lets
that child use the existing session startup WebSocket prewarm before the
first escalation. This does not introduce a Guardian-specific prewarm
mechanism.
## What changed
- initialize the existing Guardian review-session manager owned by
`Session` when a thread starts with auto-review enabled and an approval
policy that routes to Guardian
- use the standard Guardian child-session construction and the existing
session startup WebSocket prewarm
- preserve the existing reuse-key invalidation and lazy creation
fallback when startup initialization fails or the effective review
configuration changes
- add an integration test that verifies normal root-session startup
emits a Guardian `generate=false` prewarm request
## Benchmark
I compared release builds against main. Each prompt first ran a
non-escalated `sleep 3`, then requested an escalated marker command.
| binary | count | avg Guardian duration | median Guardian duration |
avg Guardian TTFT |
|---|---:|---:|---:|---:|
| origin-main | 10 | 4008.7 ms | 3949.5 ms | 3746.5 ms |
| session-fix | 10 | 2865.0 ms | 2594.0 ms | 2492.7 ms |
Guardian duration fell by 28.5% and Guardian TTFT fell by 33.5%. These
measurements cover Guardian review latency; they do not measure parent
thread-start latency.
## Description
This PR cuts Codex over from generic `ResponseItem.metadata` (introduced
here: https://github.com/openai/codex/pull/28355) to
`ResponseItem.internal_chat_message_metadata_passthrough`, which is the
blessed path and has strongly-typed keys.
For now we have to drop this MAv2 usage of `metadata`:
https://github.com/openai/codex/pull/28561 until we figure out where
that should live.
## Why
#29113 moved remote sandbox setup and enforcement to the exec server.
That gives the executor ownership of the platform-specific work: a Linux
executor chooses and runs a Linux sandbox even when the Codex
orchestrator is running on macOS or Windows.
It also means the orchestrator no longer knows which concrete sandbox
the executor selected. When that sandbox blocks a remote command, the
orchestrator currently sees only a failed process and can treat the
denial as an ordinary command failure. The existing sandbox approval and
retry path is then skipped.
This PR lets the executor report one portable fact:
> This command probably failed because the executor sandbox blocked it.
The executor keeps its concrete sandbox type private. The protocol sends
only the semantic result.
## Example
Suppose a local macOS Codex session asks a Linux devbox to write outside
the allowed workspace.
Before this PR:
```text
Linux sandbox blocks the write
-> remote process exits with "Permission denied"
-> local orchestrator sees an ordinary command failure
-> the normal sandbox approval and retry path can be skipped
```
With this PR:
```text
Linux sandbox blocks the write
-> executor reports sandboxDenied: true
-> unified exec returns UnifiedExecError::SandboxDenied
-> the existing approval prompt is shown
-> an approved retry runs through the existing unsandboxed retry path
```
## What changes
### The executor remembers its selected sandbox
The prepared remote process now retains the executor-selected
`SandboxType`. This value never crosses the executor boundary.
Commands started without a sandbox retain `SandboxType::None` and are
never reported as sandbox denials.
### The executor uses the existing denial heuristic
The existing local denial heuristic moves from `codex-core` into the
shared `codex-sandboxing` crate.
When a sandboxed remote process exits, the executor:
1. waits the same short output grace period used by local unified exec;
2. reads the output currently available in the existing retained output
buffer;
3. runs the existing heuristic using the exit code and common denial
messages;
4. stores the yes/no result before publishing the process exit.
This deliberately matches the old local unified-exec behavior. It does
not add a new streaming classifier, another output buffer, or stronger
output-retention guarantees.
### The protocol reports a portable boolean
`process/read` gains `sandboxDenied`:
```json
{
"exited": true,
"exitCode": 1,
"closed": false,
"sandboxDenied": true
}
```
The field defaults to `false` when an older executor omits it. The
response does not expose the executor sandbox implementation or
executor-native paths.
### Unified exec uses the existing error path
The exec-server client carries `sandboxDenied` into the unified process
state. If it is true, unified exec returns the existing `SandboxDenied`
error instead of trying to classify remote output using an
orchestrator-side sandbox type.
Remote process exit remains visible as soon as the process exits. This
PR does not wait for stdout or stderr to close and does not change the
existing process lifecycle.
## Scope
This PR is intentionally limited to matching the existing local
unified-exec behavior for the initial command execution path.
It does not add:
- incremental denial tracking across the full output stream;
- new denial handling for commands completed later through
`write_stdin`;
- new guarantees for preserving the semantic flag during the narrow
reconnect-recovery race.
Those can be considered separately if the same behavior is added for
local execution.
## Test coverage
One remote end-to-end integration test covers the complete intended
flow:
```text
remote read-only sandbox
-> denied write
-> executor reports the denial
-> Codex requests approval
-> user approves
-> retry succeeds on the remote executor
```
Existing lifecycle coverage continues to verify that remote process exit
is reported before late output streams close.
This PR moves construction of `PluginTelemetryMetadata` from loader and
model helpers into `PluginsManager`, which already owns installed plugin
state and will eventually perform remote identity enrichment. The
metadata type remains in `codex-plugin`, and serialized analytics events
remain unchanged.
## Before
```mermaid
flowchart LR
subgraph Events["Analytics event paths"]
direction TB
Lifecycle["Local install / uninstall"]
Config["Enable / disable"]
Remote["Remote install"]
Used["Plugin used"]
end
subgraph Construction["Metadata construction"]
direction TB
Loader["Loader telemetry helpers"]
Summary["PluginCapabilitySummary::telemetry_metadata"]
Override["Caller adds remote_plugin_id"]
end
Metadata["PluginTelemetryMetadata"]
Lifecycle --> Loader
Config --> Loader
Remote --> Loader
Loader -->|"local events"| Metadata
Loader -->|"remote install"| Override
Override --> Metadata
Used --> Summary
Summary --> Metadata
```
Telemetry metadata was constructed through loader helpers, a
capability-summary method, and a remote-install call-site override.
## After
```mermaid
flowchart LR
subgraph Events["Analytics event paths"]
direction TB
Lifecycle["Local install / uninstall"]
Config["Enable / disable"]
Remote["Remote install"]
Used["Plugin used"]
end
Manager["PluginsManager — single construction owner"]
Metadata["PluginTelemetryMetadata"]
Lifecycle --> Manager
Config --> Manager
Remote -->|"authoritative remote ID"| Manager
Used -->|"capability summary"| Manager
Manager --> Metadata
```
Every analytics path delegates metadata construction to
`PluginsManager`. Remote install still supplies its authoritative
backend ID explicitly.
## What Changes
- Make loader code return a focused plugin capability summary instead of
constructing analytics metadata.
- Centralize immutable plugin telemetry metadata construction in
`PluginsManager`.
- Route local install/uninstall, remote install, enable/disable, and
plugin-used emitters through the manager.
- Preserve the current serialized analytics contract exactly.
Normal metadata still has no remote override. Remote install continues
to provide its authoritative backend ID explicitly, so the existing
serializer continues reporting that ID through `plugin_id`.
Snapshot-based enrichment is intentionally deferred to the final PR.
## Testing
- `just test -p codex-core-plugins` (238 tests passed)
- `just test -p codex-plugin` (3 tests passed)
- Scoped Clippy/compile checks passed for `codex-plugin`,
`codex-core-plugins`, `codex-app-server`, and `codex-core`.
## Split Overview
```text
main
├── #27093 Debug analytics capture (merged)
├── #27099 Non-mutating plugin smoke (merged)
├── #27100 Remote install/uninstall smoke (merged)
└── #27102 Plugin telemetry metadata refactor ← you are here
└── #27669 Persist remote plugin identity
After #27102 and #27669 merge:
└── Final PR: add explicit local and remote IDs to plugin analytics
```
Review order and dependencies:
1. [#27093 Add debug-only analytics event
capture](https://github.com/openai/codex/pull/27093) (merged)
2. [#27099 Add a plugin analytics smoke
workflow](https://github.com/openai/codex/pull/27099) (merged)
3. [#27100 Add a remote plugin analytics mutation smoke
workflow](https://github.com/openai/codex/pull/27100) (merged)
4. This metadata refactor, independent and based on `main`
5. [#27669 Persist remote plugin
identity](https://github.com/openai/codex/pull/27669), stacked on this
PR
6. Final remote-ID behavior PR, created after the prerequisites merge
The original [#26281](https://github.com/openai/codex/pull/26281)
remains open as the aggregate reference until the final replacement PR
is published.
## What
- make Fjord's centralized response-item image preparation unconditional
for new and resumed history
- have local user images and `view_image` outputs always defer decoding
and resizing to that path
- retain `resize_all_images` as an ignored, removed compatibility key
for released clients
- delete the flag-off producer paths and obsolete policy-specific tests
## Why
Centralized preparation is now the intended image path. Keeping the
runtime feature checks also kept two image-processing implementations
alive and allowed client config to select the legacy behavior.
This is a clean replacement for #28975, rebuilt from the latest `main`.
## How
`prepare_response_items` now runs whenever items enter history and
whenever persisted history is reconstructed. Producers emit deferred
image data, so malformed images become the existing model-visible
placeholder instead of failing the session at the producer.
## Test plan
- `just fmt`
- `just fix -p codex-core -p codex-features`
- `just test -p codex-features` — 52 passed
- focused affected `codex-core` set — 20 passed
- `just test -p codex-core handle_accepts_explicit_high_detail` — 1
passed
- full `just test -p codex-core` attempt — 2,723 passed; 88 unrelated
environment failures from read-only `~/.codex` SQLite state and
unavailable integration helper binaries
## Description
Restore `thread_source` in `x-codex-turn-metadata`.
Inadvertently removed `thread_source` from `x-codex-turn-metadata` in
https://github.com/openai/codex/pull/27122 - didn't realize it was a
top-level thread app-server API field, not passed in
`responsesapi_client_metadata`.
This also reserves the key so `responsesapi_client_metadata` cannot
override it.