## What changed
- Emit `codex_guardian_v2_classification` events with the outcome, risk
level, duration, model, and thread attribution.
- Emit `codex_guardian_v2_fast_decision` events when Guardian V2 approves an
action without a full review.
- Add `guardian_v2_enabled` to turn analytics, accounting for review scope
and model requirements.
## Testing
- Extend the Guardian V2 app-server coverage to verify event payloads,
attribution, enablement state, and scoped event emission.
GitOrigin-RevId: 0805975f8e8f9d954aa1058ea8c60545a090ae74
## What changed
- Generate a UUIDv7 turn ID for each Guardian classification and preserve it across retries.
- Record the owning turn as `parent_turn_id` and propagate its trusted `root_turn_id` when known, including through the tool lifecycle extension API.
- Omit ambiguous or unknown root lineage and keep classification identities distinct when reusing Guardian sessions and WebSocket connections.
## Testing
- Cover classification metadata across retries, connection reuse, concurrent samples, auth refresh, and consecutive owning turns.
GitOrigin-RevId: 78757f2ad031844e4b545802f634c678a9ee2298
## What changed
- Add `ToolLifecycleContributor::on_mcp_tool_result` with access to the
executed MCP tool context, rewritten arguments, extension data stores, and
mutable server result.
- Run contributors before publishing MCP completion and before preparing the
result for the model, so extensions can inspect or replace successful and
error results.
## Testing
- Cover unchanged, replaced, and error results for direct MCP calls, plus
replaced and error results through Code Mode.
- Verify completion waits for result processing and the processed result flows
to both completion events and subsequent model input.
GitOrigin-RevId: ece7acfe9c5eed4778cb78a29c3ffd06a74e1bcf
## Why
Plugin MCP trust decisions must describe the exact host-discovered server
registration. Looking up a plugin again by ID can resolve a different root.
## What changed
- Retain the host plugin root in MCP catalog attribution and connection identity.
- Reconnect an MCP server when its attributed plugin root changes.
- Pass the retained root through tool lifecycle attribution and use it directly
when building trusted tool context.
## Testing
Add coverage for catalog identity, connection reconciliation, lifecycle
attribution, and rejecting a different root associated with the same plugin ID.
GitOrigin-RevId: bc92743dd40ab44065a07c3ea972b4956ef8351f
## What changed
- Add callback lifetimes to extension `ToolCall`, `ToolEnvironment`, turn-input context, and skill-read request types.
- Require extension tool executors to handle calls for any invocation lifetime and tie their returned futures to that lifetime.
- Update built-in extensions, adapters, and tests to use the lifetime-scoped APIs without retaining host-owned environment capabilities.
GitOrigin-RevId: aca9ce3a1b6870df550bc84ec9308cc65533f161
## What changed
- Add optional `McpToolContext` metadata to `ToolStartInput`, exposing the
model-visible MCP tool details and its source classification without exposing
the executable client.
- Classify MCP calls as connectors, configured servers, plugin servers,
executor-selected plugins, or other registrations based on the prepared call.
- Prepare each MCP call before notifying tool lifecycle contributors and reuse
that same call for execution so the callback describes the call that runs.
## Testing
- Cover host-owned connector calls and extension-owned Apps server calls,
including their distinct provenance and executed tool names.
GitOrigin-RevId: efd23f511b1045ffdd96349d621ad62365569b2b
## Why
Selected executor plugin roots could still expose capabilities when managed
requirements disabled the `plugins` feature.
## What changed
- Suppress MCP servers, skills, apps, and connectors from selected executor
plugins when `plugins` is disabled, while preserving the selected-root
identity used to filter those capabilities.
- Apply the policy consistently to direct selected-root discovery and batched
executor capability discovery.
## Testing
- Add contributor-level and app-server coverage that verifies disabled plugin
capabilities are absent and their MCP servers never start.
GitOrigin-RevId: e30bd8a936c4b0e083908b89a65f4d02e01747de
## What changed
- Add a `ContextWindow` prompt slot for thread-scoped extension contributions.
- Include fragments in that slot in the token-budget context-window message.
- Keep the MCP `notes/thread_hint` bridge when the native history-notes extension is disabled, without falling back to it after a native request failure.
GitOrigin-RevId: befcb7cc8570b846094b2b542a112b186b39a30a
## What changed
- Split the approval review contributor into a `fast_decision` path for existing evidence and a `full_review` path for structured reviews.
- Add host-owned full-review inputs with action evidence, conversation history, thread and turn identifiers, approval context, and extension data.
- Add structured assessment and error types, and resolve full reviews using the first contributor that claims the request.
## Testing
- Cover full-review input propagation, first-claim ordering, and short-circuit behavior in the extension registry tests.
GitOrigin-RevId: 6f763d874af8c0b66a0d760b7b7c7dfe32251c9e
## What changed
- Add `ToolCallSource` to extension `ToolCall`s and propagate direct or Code Mode
invocation metadata, including the runtime cell and nested tool-call IDs.
- Add `ToolCall::response_byte_budget` so extension tools can bound direct-call
responses by the host truncation policy while allowing Code Mode responses up
to the tool's own limit.
- Share the skills extension's response-size limit between its `list` and `read`
tools.
## Testing
- Verify that Code Mode source metadata is preserved when core constructs an
extension tool call.
GitOrigin-RevId: b10aef01be30d35b67bee3af24a1a4175c9ee112
## What changed
- Carry each contextual fragment's `ContentItemKind` through initial context assembly and world-state updates.
- Attach the classifications to merged response messages through `content_item_kinds`, preserving their order alongside the message content.
- Convert extension prompt fragments into annotated developer fragments and classify built-in developer and guardian instructions.
## Testing
- Add an integration test covering roles and content annotations in the first request.
- Update world-state and persisted-history tests to account for annotation metadata.
GitOrigin-RevId: 71c48dad5b17bffd47ba03263e3eb6d5e74fbf4b
## What changed
- Require each `PromptFragment` to include a producer-owned `ContentItemKind`.
- Re-export `ContentItemKind` from `codex_extension_api` and assign stable kinds to skills, memories, examples, and test contributors.
- Limit prompt slots to developer policy and developer capabilities, and combine contributed fragments into a single developer update.
## Testing
- Update extension registry tests to verify fragment ordering with the new content-kind metadata.
GitOrigin-RevId: ef8624a9b4ae9dce025bca9c0954d91dd8075e85
## What changed
- Add an extension API for spawning host-owned internal sessions and a
`ThreadReadyInput` lifecycle callback that runs after thread registration.
- Add Guardian reviewer session scaffolding that records the parent thread and
effective model, plus the under-development `guardian_ext` feature flag.
- Start internal sessions with fresh history while preserving parent lineage,
shared session controls, and internal-thread visibility rules.
- Scope internal-session prompt cache keys to their source and parent thread,
and expose `guardian` as an internal session source.
## Testing
- Cover internal-session spawning, parent metadata, history isolation, prompt
cache keys, and extension spawner argument forwarding.
GitOrigin-RevId: 682dae80397d62bb36247796b447042e760ca364
## What changed
- Add environment-provided MCP restrictions for configured and plugin-provided servers.
- Disable attachment-scoped servers while their environment configuration is pending, failed, or unselected, while preserving selected-plugin access and the controller-owned Apps server.
- Apply the resolved policy to runtime startup, model tool exposure, telemetry, OAuth flows, and skill dependency installation.
## Testing
- Cover policy filtering for configured and plugin servers, environment state transitions, Apps ownership, and managed OAuth rejection.
GitOrigin-RevId: 7ca5d4dda14068e758497f88835d5cba685e159e
## What changed
- Carry selected plugin identities from MCP discovery into per-turn extension data, limited to capability roots that are ready for the turn.
- Annotate executor skill catalog entries with the matching plugin ID and `user` scope.
- Include that attribution in `skill_invocation` analytics for both explicit and implicit executor skill invocations.
## Testing
- Extend the executor skills app-server test to verify plugin ID, skill scope, and invocation type for explicit and implicit analytics events.
GitOrigin-RevId: 9345600ae29bcb8eb894fffd2d6f7b05f22ebf74
## What changed
- Invoke `ToolLifecycleContributor::on_tool_start` only after pre-tool hooks
have finalized the invocation.
- Pass hook-rewritten arguments and the post-hook conversation snapshot to the
callback.
- Skip the start callback when a hook denies execution or supplies input that
cannot be applied.
## Testing
Add lifecycle tests covering rewritten input, hook-added context, denied tool
calls, and invalid rewritten input.
GitOrigin-RevId: 936efaf4a8c35321f9982ff2a33bb8bece1ebf39
## Why
Guardian V2 needs the requested action and its conversation context to assess
risk; a tool name and call ID alone do not describe what the tool will do.
## What changed
- Expose the original, pre-hook `ToolPayload` to tool lifecycle contributors.
- Build Guardian V2 classifier input from the bounded conversation transcript
and a structured planned-action object containing the tool name and arguments.
- Warn and skip classification if the planned action cannot be serialized.
## Testing
- Verify lifecycle contributors receive tool arguments.
- Verify Guardian V2 sends transcript history and planned-action JSON to the
classifier.
GitOrigin-RevId: 28266ef7c1d2267ba705fcfbd9be4ef48e64e104
## What changed
- Add a shared, read-only `ConversationHistorySnapshot` capability to the extension API.
- Include the snapshot in `ToolStartInput`, preserving conversation order while excluding contextual user messages.
- Avoid acquiring a history snapshot when no tool lifecycle contributors are registered.
## Testing
- Verify snapshots share existing response items, remain stable as history changes, and filter contextual user messages.
- Verify tool-start callbacks receive the history available for successive tool calls.
GitOrigin-RevId: 822cae33781dd876ea628f104be08d8a449a4900
## Why
Converting each environment's working directory to a host-native path caused
turn-input contributors to omit environments with foreign path formats.
## What changed
- Expose `TurnInputEnvironment.cwd` as a `PathUri`.
- Pass every turn environment to contributors without host-path conversion.
## Testing
Added an integration test that submits a foreign working-directory URI and
verifies the contributor receives the environment unchanged.
GitOrigin-RevId: a122526be47f5331614381e9c6e4b85087a96bed
## What changed
- Add a storage-neutral queue extension for listing, adding, editing, reordering, deleting, and explicitly starting queued user messages.
- Dispatch queued messages in FIFO order when a thread becomes idle after a completed turn, while leaving them queued after interruptions or failures.
- Wait for user input to be persisted before removing its queue item, and report hook rejection, task termination, and persistence failures distinctly.
- Snapshot local image and audio attachments into validated data URLs before storing them, enforce input limits, and emit `ThreadQueueChanged` events after mutations.
## Testing
- Cover queue mutations, automatic and explicit dispatch, interruption and failure behavior, hook rejection, invalid records, input limits, and local attachment snapshotting.
GitOrigin-RevId: 14f39d7c8cbaccb767a6da84b82cced0edfcb4b9
## What changed
- Add `session_source()` to `McpServerContributionContext` for thread-scoped MCP resolution.
- Propagate each thread's `SessionSource` through initial setup, per-step resolution, and runtime refreshes.
## Testing
- Verify global contexts omit the source while thread-scoped contexts retain it.
- Verify MCP contributors observe distinct sources for a root thread and its spawned subagent.
GitOrigin-RevId: 23da1509858e023fbd9e2d179034ddc83553f17b
## What changed
- Add an optional `ExtensionMetrics` capability to thread, turn-input, and world-state contributors, backed by session telemetry in the host.
- Record skill counts, omissions, and truncated description characters for extension-rendered catalogs, tagged by the catalog surface.
- Preserve host session attribution on extension metrics, including the effective model for each turn, and prevent extension tags from overriding host metadata.
- Avoid duplicate host-catalog samples by recording world-state metrics only when the section is published or changes.
## Testing
- Cover metric values and surface tags, session metadata preservation, turn-level model changes, and host world-state publication behavior.
GitOrigin-RevId: 9059e521943a8f81df25a38b03692eddbc1d990d
## What changed
- Remove the step-scoped `ExtensionData` argument from context, turn-input, and tool contributors.
- Pass the host's optional `McpResourceClient` through `ThreadStartInput` so extensions can retain session capabilities explicitly.
- Keep the MCP resource client in skills-owned session state for catalog loading and skill tools.
GitOrigin-RevId: bafa77bcd998aff408d6a396c5fd9ac268c4cce4
## What changed
- Add an `ExtensionData` store to each `StepContext`.
- Pass the step store to context, world-state, turn-input, and tool contributors so extensions can use capabilities bound to the current sampling step.
- Preserve the captured step context when rebuilding initial context during compaction.
GitOrigin-RevId: 51f3105d3a793f60534f122f9be8247c131c2ca5
## Why
Selected capability roots can contribute plugins, MCP servers, connectors, and
skills. Discovering each contribution separately requires repeated access to the
executor filesystem.
## What changed
- Add the `capabilityRoots/discoverV1` exec-server RPC to scan selected roots and
materialize recognized plugin manifests, configuration files, skill
instructions, and skill metadata in one bounded request.
- Add the opt-in `executor_capability_discovery` feature, with a thread-scoped
cache and per-step snapshot shared by MCP and skill discovery.
- Parse MCP, connector, and skill contributions from the materialized snapshot,
including serving cached skill instructions without another filesystem read.
## Testing
- Cover discovery limits, manifest precedence, root-local failures, cache reuse,
plugin contributions, and parity with the existing environment skill loader.
GitOrigin-RevId: f98fd2321cafb58c596db02da1f83c09d8eb375d
## Why
Standalone web search and image requests need to preserve the trusted,
thread-scoped originator used for billing attribution.
## What changed
- Seed extension data with the resolved thread originator and recognize
`codex_work_cca` and `chatgpt_cca` service names as originators.
- Forward non-default originators from the web search and image generation
extensions with the shared originator-header helper.
## Testing
- Cover originator resolution, header validation, and forwarding for web
search, image generation, and image editing.
GitOrigin-RevId: 165dace8e4d24e4bd92f234bff5ec77a21a7ff6e
## What changed
- Let deferred environments provide selected capability roots with their ready signal.
- Validate that those roots have unique, non-empty IDs, belong to the registering environment, and stay within the root limit.
- Include roots from ready turn environments when resolving MCP contributions, and refresh the MCP runtime when the selected root set changes.
- Expose the exact ready root set to MCP contributors so executor plugins become available with their environment.
## Testing
- Cover ready-root propagation, validation failures, replacement isolation, reconnection, and MCP plugin availability refresh.
GitOrigin-RevId: ec3498aab1164824025094e96a9b1063b7b731ad
## What changed
- Add `SkillInvocationContributor` and registration support to the extension API.
- Provide invocation callbacks with session-, thread-, and turn-scoped extension data, the turn ID, skill resource, and explicit or implicit invocation kind.
- Notify registered contributors when Codex observes a deduplicated implicit skill invocation.
## Testing
- Extend the extension registry round-trip test to cover skill invocation contributors.
GitOrigin-RevId: f428336728889d9bf4fba41a7dd8d3a6f9b9728c
## Summary
- Forward Codexs canonical `originator` header on ChatGPT-hosted Apps
and plugin-runtime MCP requests.
- Preserve the configured `X-OpenAI-Product-Sku` header.
- Cover originator-only and originator-plus-SKU configurations.
## Why
Sites project creation is logged downstream of Apps MCP. Production
validation found `CODEX_UNKNOWN_DEFAULT` project-created threads that
matched `codex_surface=desktop_app` and `originator=Codex Desktop` in
`fact_codex_cli`
([query](https://kepler.gateway.data-1.internal.api.openai.org/permalink/H_mVoVPqLQ0)).
The hosted Apps MCP configuration forwarded the product SKU but not
Codexs canonical originator, so codex-backend could not derive
`CODEX_DESKTOP_APP` for those tool calls.
## Validation
- `just fmt`
- `CARGO_HOME=/private/tmp/codex-cargo-home
CARGO_TARGET_DIR=/private/tmp/codex-target just test -p codex-mcp` (106
passed)
- `CARGO_HOME=/private/tmp/codex-cargo-home
CARGO_TARGET_DIR=/private/tmp/codex-target just fix -p codex-mcp`
## Why
Generic plugin guidance is currently emitted only with initial context
from host plugin state. An executor-selected plugin can become available
later in the same turn, making its skills and tools usable without ever
telling the model how plugin capabilities should be used.
## What
- project every ready selected plugin package, including skill-only
plugins
- carry plugin availability with the exact MCP runtime projection while
preserving MCP manager reuse when servers and connectors are unchanged
- move generic plugin guidance from the static initial-context path into
persisted World State
- recognize legacy and retained plugin fragments so resume and
compaction do not duplicate guidance
## Testing
- `just test -p codex-mcp-extension`
- `just test -p codex-core plugins_instructions`
- `just test -p codex-core
plugin_availability_change_reuses_the_mcp_manager`
- `just test -p codex-app-server --test all selected_capabilit`
## Why
World State restores its structured snapshot on resume so unchanged
sections do not have to be rendered again. That is safe only when the
model-visible fragment represented by the snapshot is still present in
retained history.
For selected executor skills, the failing selected-capability scenario
exposed this state:
```text
persisted World State: selected skill catalog is known
retained model history: selected skill catalog message is missing
next diff: unchanged, so emit nothing
```
The model resumes without being told about the selected skill catalog.
## What changed
World State contributions may now optionally describe the concrete
model-visible fragment that must remain in retained history.
When a persisted snapshot is present:
```text
matching retained fragment exists -> trust snapshot, emit nothing
matching retained fragment missing -> treat section as absent, render current state once
```
The skills extension uses this for non-empty selected-environment
catalogs by matching its exact rendered catalog body. Empty or hidden
catalogs do not require a fragment.
## Scope
This does not clear or rebuild the whole World State baseline. It does
not change skill discovery, cache invalidation, environment
availability, or MCP runtime behavior. It only keeps a persisted section
snapshot and its retained model context consistent across resume/history
reconstruction.
## Coverage
A focused World State regression test verifies both sides:
- a missing retained fragment is rendered again
- a matching retained fragment avoids duplicate injection
## Why
Selected plugin metadata is stable, but MCP processes are live runtime
state. They need different lifetimes:
- the MCP extension caches manifest, MCP, and connector declarations for
each stable selected root;
- each model step projects that cached metadata through the roots that
resolved as ready for that exact step;
- the MCP manager is rebuilt only when that availability projection
changes.
This matches executor skills: both features consume the same resolved
step roots instead of inferring readiness from the turn's selected
environments.
## Behavior
```text
E1 not ready for this step
-> no E1 MCP servers or connectors
-> cached plugin metadata stays in ext/mcp
E1 becomes ready
-> reuse cached metadata
-> publish one MCP runtime containing E1 capabilities
same ready roots on the next step
-> reuse the exact runtime; no rediscovery and no MCP restart
resume
-> create new extension thread state and a new MCP runtime
```
All model-facing consumers use the same step snapshot:
```text
resolved selected roots
|
v
extension MCP/connector projection
|
v
{ MCP config, connector snapshot, MCP manager }
|
+-> advertise model tools
+-> build app/connector tools
+-> execute MCP calls
```
## Cache contract
The existing MCP extension owns a cache keyed by the full
`SelectedCapabilityRoot`:
```rust
let state = thread_store.get_or_init(SelectedExecutorPluginMcpState::default);
```
The cache lives with extension thread state. Environment availability
filters projection but does not invalidate metadata. Resume creates new
thread state. There is no file watcher or executor generation because
contents behind a stable environment/root are assumed stable.
## What changes
- Keeps executor plugin discovery and cached metadata in `ext/mcp`.
- Caches MCP and connector declarations together per selected root.
- Uses the step's already-resolved capability roots, including lazy
environments that are not turn environments.
- Reuses the current MCP runtime when the ready-root projection is
unchanged.
- Uses the same step MCP manager and connector snapshot for
model-visible tools and execution.
- Resolves direct thread-scoped MCP requests from the current
selected-root projection.
## Deliberately out of scope
- `app/list` remains based on the latest global host-plugin state; this
PR does not make its response or notifications thread-specific.
- `required = true` startup semantics do not apply to delayed executor
MCP activation.
- No filesystem/content invalidation.
- No transport-disconnect watcher.
- No executor generations or environment replacement semantics.
- No client sharing across complete manager replacements.
## Stack
1. Extension-owned World State sections.
2. Project executor skills through World State.
3. Pin one MCP runtime to each model step.
4. **This PR:** project selected MCP and connector state from
extension-owned metadata.
5. Integration coverage for selected capability availability and resume.
## Verification
-
`selected_plugin_servers_use_managed_requirements_for_the_selected_root_id`
- The stacked integration PR covers unavailable to ready activation,
unchanged-runtime reuse, skills, MCP tools, connector attribution, and
cold resume.
## Why
A selected executor environment can be unavailable in one model step and
ready in the next. The model should see its skills only while that
environment is ready, without rescanning stable files on every sample.
The product assumption is simple:
- an environment ID names one stable logical environment;
- the selected root contents do not change during the thread.
## Behavior
```text
E1 unavailable -> do not show E1 skills
E1 ready -> discover once, cache, show through World State
E1 unavailable -> hide skills, keep cache
E1 ready again -> reuse cache, show skills again
resume -> create a new thread cache and discover again
```
The cache key is the full `SelectedCapabilityRoot`. Availability does
not invalidate it; dropping the extension's thread state does.
The step supplies the ready selected roots directly. They do not have to
be turn environments:
```text
turn environment: laptop
selected root: worker:/plugins/lint-fix
worker ready -> lint-fix skills are visible
```
## What changes
- Keeps executor skill catalogs in the existing skills extension.
- Passes the roots resolved as ready for the step into World State
contributors.
- Loads each ready selected root at most once per thread.
- Contributes the executor catalog as the `skills` World State section.
- Uses the exact step catalog for explicit skill selection and body
reads.
- Leaves host and orchestrator skill behavior where it already lives.
Taking a step snapshot itself does not add an RPC. Executor filesystem
calls happen only on the first discovery of a stable root for that
thread.
## What does not change
- No filesystem watcher or content-based invalidation.
- No retry/generation framework.
- No skill runtime migration into core.
- No general rewrite of the skills extension.
## Stack
1. Extension-owned World State sections.
2. **This PR:** project cached executor skills through World State.
3. Pin one MCP runtime to each model step.
4. Project selected MCP/app/connector metadata by environment
availability.
5. One end-to-end integration scenario.
## Why
#29856 already owns the durable thread intent and exact environment
binding. This PR adds only the small missing extension boundary: an
extension can contribute one named World State section, while core still
owns persistence, diffing, and model-visible fragment types.
This lets skills stay in the skills extension instead of moving their
runtime into core.
## Shape
```text
extension-owned state
|
| contribute section id + JSON snapshot + renderer
v
core World State
|
| compare with the previous snapshot
v
no message, or one incremental model-visible update
```
The extension API is deliberately small:
```rust
fn contribute_world_state(...) -> Vec<WorldStateSectionContribution>
```
Core adapts the rendered result to `ContextualUserFragment`, records the
snapshot, and keeps the existing compaction/resume behavior.
## What changes
- Adds extension-owned World State section contributions.
- Calls those contributors from the existing per-step World State
builder.
- Restores durable selected capability roots into extension thread state
on resume.
- Keeps the actual model-context fragment and rollout machinery in core.
## What does not change
- No skill or MCP implementation moves out of its extension.
- No new file watcher, generation, or RPC.
- No generic migration of existing World State sections.
- No change to the stable environment-ID assumption from #29856.
## Example
```text
step 1 snapshot: skills = []
step 2 snapshot: skills = [executor-demo:deploy]
core asks the skills extension to render only that change.
```
## Stack
1. **This PR:** let extensions contribute World State sections.
2. Project executor skills through the skills extension.
3. Pin one MCP runtime to each model step.
4. Project selected MCP/app/connector metadata by environment
availability.
5. One end-to-end integration scenario.
## Summary
- keep context injection on a single ContextContributor trait
- split context injection into thread-scoped and turn-scoped
contribution methods
- wire turn-scoped fragments into initial context assembly so extensions
can contribute context from turn-local state
## Why
App-server threads without a local executor need orchestrator-owned
skills from the hosted `codex_apps` MCP server. Threads with the local
executor already discover installed skills from the local filesystem.
After the orchestrator skill provider was enabled for every app-server
thread, local-executor threads also received the hosted skill catalog
and the `skills.list` and `skills.read` tools. This changed the existing
local behavior and could expose a second hosted copy of a skill that was
already installed locally.
## What changed
- Expose the thread's selected execution environments to extensions at
thread startup.
- Enable orchestrator skills only when the reserved local environment is
not selected.
- Apply that decision consistently to hosted skill catalog discovery,
explicit skill injection, and the `skills.list` and `skills.read` tools.
## Verification
- The existing no-executor app-server test continues to verify hosted
skill discovery, invocation, and child-resource reads.
- A new app-server test verifies that local-executor threads do not
receive hosted skill context or `skills.*` tools.
## Why
**In short:** this PR resolves already-discovered MCP registrations. It
does not read selected plugins or discover their MCP servers.
The resolved MCP catalog currently builds config and auto-discovered
plugin registrations before runtime contributors are applied. A
thread-selected plugin needs a distinct precedence tier in that same
initial resolution pass: otherwise a disabled lower-precedence winner
can leave stale name-level state behind, and the winning MCP tools
cannot be attributed to the selected package reliably.
This PR adds that catalog boundary before executor discovery is
connected.
## What changed
- Added an explicit selected-plugin registration tier between
auto-discovered plugins and explicit config.
- Collected selected-plugin contributions before the initial catalog
build, while leaving compatibility and generic extension overlays in
their existing runtime phase.
- Retained the winning plugin ID and display name directly on
plugin-owned catalog registrations.
- Derived MCP tool provenance from the winning catalog entry instead of
joining against local-only plugin summaries.
- Retained the winning selected server's tool approval policy in the
running connection manager, so a selected registration cannot inherit
approval behavior from a losing local plugin.
- Kept remembered approval session-scoped for selected plugins until
there is an authority-aware persistence contract; Codex will not write
approval back to an unrelated local plugin.
- Preserved existing name-level disabled vetoes for discovered plugins
and config, while keeping a selected package's own disabled registration
scoped to that registration.
- Preserved deterministic selection order and existing config,
compatibility, and extension precedence.
The resulting order is:
```text
auto-discovered plugin
< selected plugin
< explicit config
< compatibility registration
< extension overlay
```
## Behavior and scope
This is a catalog and provenance change only. No production host
contributes selected-plugin MCP registrations yet, so existing local MCP
behavior remains unchanged.
The stacked follow-up, #27870, installs the executor plugin provider
that produces these registrations. App-server activation remains a
separate final step.
## Verification
Focused tests cover precedence, deterministic selected-plugin conflicts,
disabled-veto behavior across catalog phases, managed requirements
before selected-plugin resolution, winning-server approval policy, and
attribution when local and selected packages share an ID or server name.
CI owns execution of the test suite.
## Why
`selectedCapabilityRoots` belongs to one thread, but MCP contributors
previously received only the global Codex config. That left no clean way
for a selected executor capability to contribute MCP servers to its own
thread.
## What this PR does
- Gives MCP contributors a small context containing the config and, for
a running thread, its frozen host-seeded inputs.
- Uses the same thread inputs during startup, status queries, refreshes,
and skill dependency checks.
- Keeps threadless MCP operations and the existing hosted Apps behavior
unchanged.
- Adds coverage showing that two threads resolve independent
registrations and that later lifecycle mutations do not change the
frozen MCP inputs.
This PR does not discover plugin manifests, add MCP servers, or launch
anything new. It only establishes the thread-scoped registration
boundary.
## Follow-ups
- Resolve selected executor plugin roots through their owning
environment filesystem.
- Convert their stdio MCP declarations into environment-bound
registrations and add an executor MCP end-to-end test.
## Verification
- `just fmt`
- `cargo check --tests -p codex-protocol -p codex-extension-api -p
codex-mcp-extension -p codex-core -p codex-app-server`
Tests and Clippy were not run.
## Stack
- Base: #27191
- This PR is the third vertical and should be reviewed against
`jif/external-plugins-2`, not `main`.
## Why
#27191 moves the host-owned Apps MCP registration behind an extension
contributor, but deliberately preserves the existing endpoint-selection
feature while that contribution contract lands. App-server can therefore
resolve the server through extensions, yet the hosted plugin endpoint is
still selected through temporary `apps_mcp_path_override` plumbing.
That is not the long-term plugin model. A plugin can bundle skills,
connectors, MCP servers, and hooks, and those components do not all need
the same source or execution environment. In particular, an
authenticated HTTP MCP server can expose plugin capabilities directly
from a backend without an executor or an orchestrator filesystem.
This PR completes that hosted vertical. App-server's MCP extension now
owns the aggregate hosted plugin runtime at `/ps/mcp`. Connector actions
continue to arrive as MCP tools, while backend-provided skills arrive as
MCP resources and use Codex's existing resource list/read paths. No
second backend client, skill filesystem, or generic plugin activation
framework is introduced.
The backend route remains the hosted implementation. This change
replaces Codex's temporary endpoint-selection mechanism, not the service
behind the endpoint.
## What changed
### Hosted plugin runtime
The MCP extension now contributes `codex_apps` as the hosted plugin
runtime rather than as a configurable Apps endpoint:
- `https://chatgpt.com` resolves to
`https://chatgpt.com/backend-api/ps/mcp`;
- a bare custom ChatGPT base resolves to `/api/codex/ps/mcp`;
- the existing product-SKU header and ChatGPT authentication behavior
are preserved;
- executor availability is never consulted for this streamable HTTP
transport.
The same MCP connection carries both component shapes supported by the
hosted endpoint:
- connector actions are discovered and invoked as MCP tools;
- hosted skills are enumerated and read as MCP resources through the
existing `list_mcp_resources` and `read_mcp_resource` paths.
This keeps component access in the subsystem that already owns the
protocol instead of downloading backend skills into an orchestrator
filesystem or inventing a parallel hosted-skill client.
### Explicit runtime ordering
`McpManager` now resolves the reserved `codex_apps` entry in three
ordered phases:
1. install the legacy Apps fallback for compatibility;
2. apply ordered extension `Set` or `Remove` overlays;
3. apply the final ChatGPT-auth gate without synthesizing the server
again.
This ordering is important:
- an ordinary configured or plugin MCP server cannot claim the
auth-bearing `codex_apps` name;
- an extension-contributed hosted runtime wins over the fallback;
- an extension `Remove` remains authoritative;
- a host without the MCP extension retains the legacy Apps endpoint and
current local-only behavior.
The temporary `legacy_apps_mcp_loader_enabled` coordination flag is no
longer needed.
### Remove the path override
The `apps_mcp_path_override` feature and its runtime plumbing are
removed, including:
- the feature registry entry and structured feature config;
- `Config` and `McpConfig` fields;
- config schema output;
- config-lock materialization;
- URL override handling in `codex-mcp`.
Existing boolean and structured forms still deserialize as ignored
compatibility input. They are omitted from new serialized config, and
config-lock comparison normalizes the removed input so older locks
remain replayable.
### App-server coverage
App-server MCP fixtures now serve the hosted route at
`/api/codex/ps/mcp`. Existing resource-read and tool/elicitation flows
therefore exercise the extension-owned endpoint rather than succeeding
through the legacy fallback.
The stack also adds the missing `codex_chatgpt::connectors` re-export
for the manager-backed connector helper introduced in #27191.
## Compatibility
- App-server installs the extension and uses `/ps/mcp` for the hosted
runtime.
- CLI and other hosts that do not install the extension retain the
legacy Apps endpoint.
- Apps disabled or non-ChatGPT authentication removes `codex_apps` from
the effective runtime view.
- Existing local plugins, local skills, executor-selected skills,
configured MCP servers, and MCP OAuth behavior are otherwise unchanged.
- Backend plugin enablement remains account/workspace state owned by the
hosted endpoint; this PR does not add thread-local backend plugin
selection.
## Architectural fit
The stack now proves two independent runtime shapes:
1. #27184 resolves filesystem-backed skills through the executor that
owns a selected root.
2. #27191 and this PR resolve a backend-hosted HTTP MCP through an
extension with no executor.
Together they preserve the intended separation:
- selection identifies a plugin/root when explicit selection is needed;
- each component's owning extension resolves its concrete access
mechanism;
- execution stays with the runtime required by that component;
- existing skills, MCP, connector, and hook subsystems remain the
downstream consumers.
## Planned follow-ups
1. **Executor stdio MCP:** selecting an executor plugin registers a
manifest-declared stdio MCP server and executes it in the environment
that owns the plugin.
2. **Optional backend selection:** only if CCA needs thread-local
selection distinct from backend account/workspace enablement, add a
concrete backend-owned capability location and surface those selected
skills through the skills catalog.
3. **Connector metadata and hooks:** activate those plugin components
through their existing owning subsystems, with executor hooks remaining
environment-bound.
4. **Propagation and persistence:** define explicit resume, fork,
subagent, refresh, and environment-removal semantics once selected roots
have multiple real consumers.
5. **Local convergence:** migrate legacy local skill, MCP, connector,
and hook paths behind their owning extensions one vertical at a time,
then remove duplicate core managers and compatibility plumbing after
parity.
## Verification
Coverage in this change exercises:
- extension-owned `/backend-api/ps/mcp` registration without an
executor;
- preservation of the legacy endpoint in hosts without the extension;
- extension `Set` and `Remove` precedence over the legacy fallback;
- ChatGPT-auth gating for the reserved server;
- hosted MCP resource reads with and without an active thread;
- connector tool invocation and MCP elicitation through the hosted
route;
- ignored boolean and structured forms of the removed path override;
- config-lock replay compatibility for the removed feature.
`cargo check -p codex-features -p codex-mcp-extension -p
codex-app-server` passes. Tests and Clippy were not run locally under
the current development instruction; CI provides the full validation
pass.
## Stack
- Base: #27184
- This PR is the second vertical and should be reviewed against
`jif/external-plugins-1`, not `main`.
## Why
CCA is moving toward a split runtime where the orchestrator may have no
filesystem or executor, but it still needs to activate remotely hosted
plugin components. HTTP MCP servers are the simplest complete example:
they need configuration and host authentication, but they do not need an
executor process.
The Apps MCP endpoint is currently synthesized by a special-purpose
loader inside the MCP runtime. That works locally, but it leaves hosted
MCP activation outside the extension model being established in #27184.
It also makes the Apps path a poor foundation for plugins whose skills,
MCP servers, connectors, and hooks may come from different sources or
execute in different places.
This PR moves that one behavior behind an extension-owned contribution
while preserving the existing local fallback. It deliberately does not
introduce a generic plugin activation framework.
## What changed
### MCP extension contribution
`codex-extension-api` gains an ordered `McpServerContributor` contract.
A contributor returns typed `Set` or `Remove` overlays for MCP server
configuration; later contributors win for the names they own.
The contract stays at the existing MCP configuration boundary.
Extensions do not create a second connection manager or transport
abstraction.
### Hosted Apps MCP extension
A new `codex-mcp-extension` contributes the reserved `codex_apps` server
from the existing Apps feature, ChatGPT base URL, path override, and
product SKU configuration.
When `apps_mcp_path_override` is enabled for `https://chatgpt.com`, the
resulting streamable HTTP endpoint is
`https://chatgpt.com/backend-api/ps/mcp`. The existing ChatGPT-auth gate
remains authoritative, so this server can run in an orchestrator-only
process without being exposed for API-key sessions.
### One resolved runtime view
`McpManager` now distinguishes three views:
- **configured:** config- and plugin-backed servers before extension
overlays;
- **runtime:** configured servers plus host-installed extension
contributions;
- **effective:** runtime servers after auth gating and compatibility
built-ins.
App-server installs the hosted MCP extension and uses the runtime view
for thread startup, refresh, status, threadless resource reads,
connector discovery, and MCP OAuth lookup. This keeps
`mcpServer/oauth/login` consistent with the servers exposed by the other
MCP APIs. The hosted Apps server itself continues to use existing
ChatGPT host authentication rather than MCP OAuth.
## Compatibility
Hosts that do not install the MCP extension retain the existing Apps MCP
synthesis path. This preserves current local-only, CLI, and
standalone-host behavior while app-server exercises the extension path.
Disabling Apps removes the reserved `codex_apps` entry, and losing
ChatGPT auth removes it from the effective runtime view. Executor
availability is not consulted for this HTTP transport.
## Follow-ups
The next vertical will resolve a manifest-declared stdio MCP server from
an executor-selected plugin root and execute it in the environment that
owns that root. Later verticals can add backend-owned skills, connector
metadata, hooks, durable selection semantics, and incremental local
convergence without changing the component-specific runtime boundaries
introduced here.
## Verification
Focused coverage was added for:
- contributing the hosted Apps MCP at `/backend-api/ps/mcp` without an
executor;
- requiring ChatGPT auth in the effective runtime view;
- removing a reserved configured Apps server when the Apps feature is
disabled.
`cargo check -p codex-app-server -p codex-mcp-extension -p
codex-extension-api -p codex-mcp` passed. Tests and Clippy were not run
locally under the current development instruction; CI provides the full
validation pass.
## Disclaimer
Do not use for now
## Why
Extensions can already contribute prompt fragments and request same-turn
item injection, but there was no host-owned hook for contributing
structured `ResponseItem`s while Codex is assembling a new turn's
initial model input. This change adds that seam so extensions can attach
turn-local input that depends on the submitted user input and resolved
turn environments without routing through prompt text or late injection.
## What changed
- add `TurnInputContributor` to `codex_extension_api` and export the new
`TurnInputContext` / `TurnInputEnvironment` types it receives
- teach `ExtensionRegistry` to register and expose turn-input
contributors alongside the existing extension hooks
- call registered turn-input contributors from
`core/src/session/turn.rs` while building the initial injected input for
a turn, then append their returned `ResponseItem`s after the skill and
plugin injections
Summary
- Add TurnErrorInput and TurnLifecycleContributor::on_turn_error to the
extension API.
- Emit the turn-error lifecycle from core turn error paths, including
usage limit failures.
- Add direct lifecycle coverage for the emitted error facts and stores.
Tests
- just fmt
- git diff --check
- Not run: full tests or clippy (per instructions)
Summary: add session source and persistent-state availability to
ThreadStartInput; populate them from session init; update existing goal
test harness constructors. Tests: just fmt; git diff --check. No full
tests or clippy run per request.
## Why
Extensions can currently observe thread start, resume, and stop, but
they do not have a lifecycle point for the host to say that immediately
pending thread work has drained. That makes idle follow-up behavior
harder to express as extension-owned logic instead of host-specific
plumbing.
This adds an explicit idle lifecycle hook so an extension can react when
a thread becomes idle while the host keeps ownership of whether any
submitted follow-up input starts a turn, is queued, or is ignored.
## What changed
- Added `ThreadIdleInput` with access to the session-scoped and
thread-scoped extension stores.
- Added a default `on_thread_idle` method to
`ThreadLifecycleContributor`.
- Re-exported `ThreadIdleInput` from the extension API surface.
## Testing
Not run; this only extends the extension API trait surface with a
default hook and exported input type.
## Why
The goal extension needs more context when a turn starts than
`turn_store` alone provides.
In particular, goal accounting needs the stable turn id, the effective
collaboration mode, and the cumulative token-usage baseline captured at
turn start so it can:
- suppress goal accounting for plan-mode turns
- compute exact per-turn deltas from cumulative `total_token_usage`
snapshots instead of relying on the most recent usage event alone
- keep the extension-owned accounting path aligned with the host turn
lifecycle
## What
- extend `codex_extension_api::TurnStartInput` to expose `turn_id`,
`collaboration_mode`, and `token_usage_at_turn_start`
- pass the full `TurnContext` plus the captured token-usage baseline
through the turn-start lifecycle emission path
- initialize goal turn accounting from the turn-start baseline and
collaboration mode
- switch goal token accounting to compute deltas from cumulative
`total_token_usage` snapshots
- add coverage for the new turn-start lifecycle fields and for
goal-accounting baseline behavior
## Testing
- added `turn_start_lifecycle_exposes_turn_metadata_and_token_baseline`
in `codex-rs/core/src/session/tests.rs`
- added `ext/goal/tests/accounting.rs` coverage for baseline-aware goal
accounting and plan-mode suppression
## Why
Extensions that need to track runtime progress currently have no typed
host signal for tool execution. The goal extension in particular needs
to observe tool attempts without inspecting tool payloads, owning tool
implementations, or staying coupled to core-only runtime plumbing.
This adds a narrow lifecycle contributor API for host-owned tool
execution: extensions can observe when an accepted tool call starts and
how it finishes, while policy hooks and tool handlers continue to own
payload rewriting, blocking, and execution.
Relevant code:
-
[`ToolLifecycleContributor`](https://github.com/openai/codex/blob/3ad2850ffc7d8a1da19c65a92425637a59098f1b/codex-rs/ext/extension-api/src/contributors.rs#L119)
defines the extension-facing observer contract.
-
[`tool_lifecycle.rs`](https://github.com/openai/codex/blob/3ad2850ffc7d8a1da19c65a92425637a59098f1b/codex-rs/ext/extension-api/src/contributors/tool_lifecycle.rs)
defines the typed start/finish inputs, source, and outcome enums.
- [`notify_tool_start` /
`notify_tool_finish`](https://github.com/openai/codex/blob/3ad2850ffc7d8a1da19c65a92425637a59098f1b/codex-rs/core/src/tools/lifecycle.rs)
bridges core tool dispatch into the extension registry.
## What Changed
- Added `ToolLifecycleContributor` to `codex-extension-api`, including:
- `ToolStartInput`
- `ToolFinishInput`
- `ToolCallSource`
- `ToolCallOutcome`
- Added registration and lookup support on `ExtensionRegistryBuilder` /
`ExtensionRegistry`.
- Wired core tool dispatch to notify lifecycle contributors for:
- accepted tool starts
- completed tool calls, including the tool output success marker
- pre-tool-use blocks
- failures before or after the handler runs
- cancellation/abort in the parallel tool path
- Registered the goal extension as a lifecycle contributor and added the
outcome filter it will use for goal progress accounting.
## Test Coverage
- Added `dispatch_notifies_tool_lifecycle_contributors` to cover
lifecycle notification ordering and outcomes for successful and
handler-failed tool calls.
## Why
The tool runtime path still had a typed output associated type on
`ToolExecutor`, plus a core-only `RegisteredTool` adapter and
extension-only executor aliases. That made every new shared tool runtime
carry extra adapter plumbing before it could participate in core
dispatch, extension tools, hook payloads, telemetry, and model-visible
spec generation.
This PR moves output erasure to the shared executor boundary so core and
extension tools can use the same execution contract directly.
## What Changed
- Changed `codex_tools::ToolExecutor` to return `Box<dyn ToolOutput>`
instead of an associated `Output` type.
- Removed the extension-specific `ExtensionToolExecutor` /
`ExtensionToolOutput` aliases and exposed `ToolExecutor<ToolCall>` plus
`ToolOutput` through `codex-extension-api`.
- Reworked core tool registration around `CoreToolRuntime` and
`ToolRegistry::from_tools`, removing the extra `RegisteredTool` /
`ToolRegistryBuilder` layer.
- Consolidated model-visible spec planning and registry construction in
`core/src/tools/spec_plan.rs`, including deferred tool search and
code-mode-only filtering.
- Added `ToolOutput` helpers for post-tool-use hook ids and inputs so
MCP, unified exec, extension, and other boxed outputs preserve the same
hook payload behavior.
- Updated core handlers, memories tools, and the related
registry/spec/router tests to use the simplified contract.
## Test Coverage
- Updated coverage for tool spec planning, registry lookup, deferred
tool search registration, extension tool routing, post-tool-use hook
payloads, dispatch tracing, guardian output extraction, and memories
extension tool execution.
## Why
`codex_tools::ToolExecutor` keeps a tool spec attached to its runtime
handler, but extension tools still carried a parallel
`ExtensionToolFuture` / `ExtensionToolExecutor` shape. That made
extension-owned tools look different from host tools even though
routing, registration, and execution need the same abstraction.
This PR makes the shared executor contract directly async and lets
extension tools implement it too, so host tools and extension tools can
move through the same registration path.
## What changed
- Changed `ToolExecutor::handle` to an `async fn` using `async-trait`,
and updated built-in tool handlers to implement the async trait
directly.
- Replaced the bespoke `ExtensionToolFuture` contract with a marker
`ExtensionToolExecutor` over `ToolExecutor<ToolCall, Output =
JsonToolOutput>`, re-exporting `ToolExecutor` from
`codex-extension-api`.
- Updated the memories extension tools to implement the shared executor
trait.
- Split tool-router construction into collected executors plus hosted
model specs, keeping hosted tools like web search and image generation
separate from executable handlers.
- Updated spec/router tests and extension-tool stubs for the new
executor shape.
## Verification
- Not run locally.
## Summary
- add a scoped level_id to ExtensionData and expose it through
level_id()
- remove thread_id/turn_id parameters from extension contributor inputs
where the scoped ExtensionData already carries that identity
- move turn-scoped extension data onto TurnContext so token usage and
lifecycle contributors can share the same turn store
## Testing
- cargo check -p codex-extension-api -p codex-core --tests
- cargo test -p codex-extension-api
- cargo test -p codex-guardian
- cargo test -p codex-core --lib
record_token_usage_info_notifies_extension_contributors
- cargo test -p codex-core --lib
submission_loop_channel_close_emits_thread_stop_lifecycle
- cargo test -p codex-core --lib
submission_loop_channel_close_aborts_active_turn_before_thread_stop_lifecycle
- just fix -p codex-extension-api
- just fix -p codex-guardian
- just fix -p codex-core
- just fmt
## Note
- Attempted cargo test -p codex-core; it aborted in
agent::control::tests::spawn_agent_fork_last_n_turns_keeps_only_recent_turns
with the existing stack overflow before the full suite completed.
## Why
Extensions can already contribute prompt, tool, turn-item, and
thread-lifecycle behavior, but there was no explicit host-owned hook for
per-turn setup and cleanup. That makes extension-private turn state
awkward: an extension either has to stash it outside the turn lifecycle
or depend on core runtime objects.
This adds a small turn lifecycle boundary. Extensions receive stable
identifiers plus the existing session, thread, and turn `ExtensionData`
stores, while core keeps owning task scheduling, cancellation, and turn
teardown.
## What Changed
- Added `TurnLifecycleContributor` with `on_turn_start`, `on_turn_stop`,
and `on_turn_abort` callbacks in `codex-rs/ext/extension-api`.
- Added typed `TurnStartInput`, `TurnStopInput`, and `TurnAbortInput`
payloads that expose `thread_id`, `turn_id`, `session_store`,
`thread_store`, and `turn_store`.
- Registered and re-exported turn lifecycle contributors through
`ExtensionRegistry` and `ExtensionRegistryBuilder`.
- Wired `Session` to emit turn start, stop, and abort callbacks from the
existing turn/task lifecycle paths.
- Carried the turn-scoped `ExtensionData` through `RunningTask` and
`RemovedTask` so stop/abort callbacks receive the same turn store
created at turn start.
## Verification
- Not run locally.