## What changed
- Remove `template_id` from MCP tool call items and lifecycle events.
- Remove `templateId` from app-server `McpToolCallAppContext` responses and generated schemas.
- Update protocol conversions, documentation, and existing test expectations for the revised metadata shape.
GitOrigin-RevId: f2b1304eadff661eec3966673ea71206b1542b98
## Why
Token-budget sessions need a final opportunity to preserve important state before the current context is compacted.
## What changed
- When `auto_compact_fallback_prompt` is configured, reserve `auto_compact_fallback_buffer_tokens` beyond the base auto-compaction limit.
- At the base limit, inject the fallback prompt once as a developer message while keeping the normal tool surface available. Continue reporting zero base-window tokens during this reserve.
- Roll over when the reserve or full model context is exhausted, and skip the fallback when a new context was already requested.
## Testing
Added coverage for using the fallback reserve, rolling over after it is exhausted, and bypassing the fallback for an explicit `new_context` request.
GitOrigin-RevId: 32983ecadc5cb4927e15ecd7b1e896cb52a9ad3b
## What changed
- Replace the combined `Codex` wrapper with an `Arc<Session>` and a dedicated
`SessionIo` handle for submissions, events, status, and loop termination.
- Make `Session::spawn` return the session and I/O handle separately, and have
`CodexThread` own both.
- Update thread management, delegated sessions, guardian reviews, and agent
control to use the appropriate session or I/O interface.
GitOrigin-RevId: 8523bad7e7366e2969fd0cc3849b4247b141427a
## Why
Core filesystem permission models operate on native absolute paths, while exec
sandbox contexts must serialize paths as portable URIs.
## What changed
- Make the core filesystem permission and profile types concrete over
`AbsolutePathBuf` instead of generic over their path representation.
- Add exec-specific permission types that convert native permission paths to
`PathUri` values at the sandbox protocol boundary.
- Replace the legacy read/write-roots tuple with a named
`LegacyReadWriteRoots` struct.
## Testing
- Add coverage that exec sandbox permission paths serialize as URIs and round
trip through the filesystem protocol.
GitOrigin-RevId: a0ef5f8aba9bbfdf00a00fcc4f199314836bc85d
## What changed
Document that `exec_command.yield_time_ms` has an effective range of
2,000–30,000 ms on Windows, and update the tool specification test to match.
GitOrigin-RevId: aa20bbb2ad82d162d29d93f3bfbe1b970445f44e
## What changed
- Define `SleepItem` in `codex-extension-items` and carry `clock.sleep` events through `TurnItem::Extension`.
- Preserve sleep persistence, thread-history reconstruction, and the app-server's `type: "sleep"` schema while reusing the extension-owned type.
## Testing
- Add serialization coverage for the `clock.sleep` extension wire shape and its protocol wrapper.
- Update sleep lifecycle and thread-history tests to exercise the extension-owned item.
GitOrigin-RevId: 34bb33ed9b90a09bf4744aafdb56e019c717a168
## Why
The generic `yield_time_ms` description does not explain the Windows-specific
initial wait behavior or when callers should choose a shorter value.
## What changed
- Describe `yield_time_ms` on Windows as the maximum wait before a running
command returns a session ID, while commands that finish sooner still return
immediately.
- Recommend the 10-second default for ordinary commands and shorter waits for
intentionally long-lived or interactive processes.
- Keep the existing parameter description on other platforms.
## Testing
- Update the expected `exec_command` tool schema for both platform variants.
GitOrigin-RevId: 32f90b3a56604fcf1734cc81ba7ab2f0bec41643
## What changed
- Convert filtered MCP tool metadata into `CoreToolRuntime` instances before constructing the tool router.
- Pass those runtimes through the shared tool-planning path instead of maintaining separate direct and deferred MCP tool lists.
- Preserve MCP visibility, app policy filtering, and direct versus deferred exposure behavior.
GitOrigin-RevId: 67f7fbeecc2992318c859e038cd97938833911db
## What changed
- Accept a caller-provided `CodexAppsToolsCache` when constructing a `ThreadManager` and pass it through to the MCP manager.
- Add `ConnectorRuntimeManager::new_without_cache()` for an in-memory runtime that neither loads nor persists connector state on disk.
- Re-export `CodexAppsToolsCache` from `codex-core` and `codex-core-api` for embedders.
## Testing
- Verify that a cache-disabled connector runtime ignores existing disk state and publishes live tools without creating cache files.
GitOrigin-RevId: f52f4f27c425a8d84ad0d0987b30a6fc3c14d702
## What changed
- Return only each live agent's name and status from `list_agents`.
- Stop tracking the most recent task message in agent metadata.
GitOrigin-RevId: 91836fe8efad7ea6b7305f0b409081ec77419cfd
## Why
Multi-environment turns can run against different filesystems, so a single
thread-wide workspace-root list cannot accurately describe every selected
environment.
## What changed
- Add optional `runtimeWorkspaceRoots` to each app-server environment selection.
Omitted roots default to that environment's `cwd`, while an empty list selects
no roots.
- Carry environment-native roots through turn selection, model-visible context,
permission materialization, and tool sandboxing.
- Keep the top-level `runtimeWorkspaceRoots` field as a compatibility input when
constructing default environment selections; explicit selections use their
own roots.
## Testing
- Cover thread and turn API defaults, overrides, empty roots, and schema
round-tripping.
- Verify foreign roots remain model-visible and remote execution applies the
target environment's roots before sandbox selection.
GitOrigin-RevId: c6e7a62883c45c676c153055f46a55ee15a323a4
## What changed
- Limit `request_plugin_install` guidance to explicitly requested plugins after tool search has been exhausted.
- Exclude adjacent capabilities, broad recommendations, and plugins that only seem potentially useful.
- Clarify that `plugin_id` is the parenthesized ID from `<recommended_plugins>` and prohibit parallel tool calls.
- Keep the recommendation context focused on listing available plugins while the tool specification owns the installation criteria.
## Testing
- Update tool specification tests to verify the stricter eligibility and serialization guidance.
GitOrigin-RevId: 6ebfdd864a76316ab59a0624dc4d7d3abda39a98
## What changed
- Limit Guardian reviewer sessions with an environment to `exec_command`,
`write_stdin`, and `view_image`.
- Exclude general tool sources from Guardian reviewer tool planning.
## Testing
- Extend the Guardian request snapshot test to assert the exact model-visible
tool list.
GitOrigin-RevId: 80ba7011c23c32d163aaad906087fac396cc70fa
## Why
Embedding callers need to control whether model catalogs are persisted to disk.
## What changed
- Accept a shared models manager when constructing `ThreadManager` instead of always creating a cached manager internally.
- Add provider APIs for creating uncached model managers. OpenAI-compatible providers fetch on each `OnlineIfUncached` refresh without reading or writing `models_cache.json`.
- Keep existing app server, MCP server, sample, and test callers on the standard cached manager.
## Testing
- Verify an uncached manager fetches on every refresh.
- Verify an injected uncached manager controls thread refresh behavior and does not create `models_cache.json`.
GitOrigin-RevId: 8bb646054ecec4ccb865b2bf56249384916ea9db
## Why
`TurnContext` stored model and reasoning settings both as resolved turn fields and inside `CollaborationMode`, requiring the copies to stay synchronized when the model changed.
## What changed
- Store the mode and collaboration-mode developer instructions directly on `TurnContext`.
- Reconstruct `CollaborationMode` from those values plus the turn's current model and reasoning effort when callers need it.
- Read the mode directly for plan-mode checks, lifecycle events, and telemetry.
GitOrigin-RevId: b8fccafdd5d5cbccc84bc70d542588087ddf0093
## Why
Standalone web search can return structured result DTOs separately from its
model-facing text output. App-server clients need access to that data without
coupling Codex to every result type or field.
## What changed
- Preserve the optional `results` array from `/v1/alpha/search` as opaque JSON
through web-search completion events, thread history, and app-server
`webSearch` items.
- Keep responses from older endpoints compatible when `results` is absent, and
allow clients to ignore result types and fields they do not understand.
- Regenerate the protocol schemas and TypeScript definitions for the optional
field.
## Testing
- Cover responses with missing, empty, and forward-compatible result payloads.
- Verify standalone web-search results round-trip through app-server item
notifications and thread-item conversion.
GitOrigin-RevId: 739ef5694ece5733f7b8749ec41d6bfa3f33c8c5
## Why
Policy-blocked proxy requests must terminate the corresponding active tool call and preserve the correct approval result, including when multiple calls are running concurrently.
## What changed
- Resolve a blocked request's execution ID to its registered active network call before recording the policy denial.
- Preserve an outcome already recorded for that call, and cancel the owning call after storing a new denial.
## Testing
Expand network approval integration coverage for Guardian and user decisions, permission hooks, persisted policy amendments, concurrent calls, timeouts, cancellations, and unattributed proxy requests.
GitOrigin-RevId: 961901bbc07cc498309453ec78cda1a56ab00a4b
## What changed
- Mark `request_plugin_install` as not supporting parallel tool calls so install requests execute serially.
- Add a unit test covering the handler's parallel-call capability.
GitOrigin-RevId: 5f2c8184562bd776b0e527e5e258c61c604a0910
## What changed
Add a `command_category` tag to tool result telemetry for `exec_command` and
`shell_command` invocations. Classify parsed commands as `read`, `list_files`,
`search`, or `unknown`; use `mixed` when a script contains multiple categories.
GitOrigin-RevId: 1edacb38c7be9b03af4f2fc86746184f19067592
## What changed
- Add optional serialized Codex turn metadata to extension tool calls.
- Populate it from the current turn context and send it as the
`x-codex-turn-metadata` header on standalone web search requests.
## Testing
- Extend the app-server web search integration test to verify that client
metadata, including the model ID, reaches the standalone search request.
GitOrigin-RevId: c0950108d89a1d317ac94ccd23efcee72fffc6f0
## Why
Model overrides for `spawn_agent` must be compatible with the multi-agent backend used by the current turn.
## What changed
- Carry each model's multi-agent backend metadata into `ModelPreset`.
- Filter the advertised `spawn_agent` model overrides for multi-agent v2 and reject overrides assigned to another backend.
- Limit error suggestions to picker-visible, backend-compatible models.
## Testing
Added coverage for hiding incompatible models from the tool description and rejecting them during spawn validation.
GitOrigin-RevId: 22c12aba67df46e9743a74f019b72a1b8b76b308
## What changed
- Add the `features.multi_agent_v2.expose_spawn_agent_model_overrides` setting, enabled by default, to expose `model` and `reasoning_effort` on the v2 `spawn_agent` tool.
- Keep these controls available when other spawn metadata is hidden, while allowing them to be disabled independently.
- Add root-agent and subagent guidance that overrides require a partial or context-free fork and should only be used when explicitly authorized.
## Testing
- Cover configuration parsing and defaults, usage-hint preservation, and tool-schema behavior with override exposure enabled and disabled.
GitOrigin-RevId: 92370498108c96fbd51f32965624ce531e991d9a
## What changed
- Include permission instructions in Guardian review requests and let the review model use its configured tool mode and standard tool plan instead of a Guardian-specific direct-tool override.
- Refine the Guardian policy for tenant policy precedence, authorization scoring, prompt-injection handling, read-only investigation, and post-denial user approval.
- Simplify low-risk allow responses to `{"outcome":"allow"}` regardless of authorization scoring.
## Testing
- Update Guardian request-layout snapshots to cover the permission instructions included in initial and follow-up reviews.
GitOrigin-RevId: eb83571ef61f7b4e854c12e13e7163c6fab43f00
## What changed
- Add an optional `error` payload to `TurnCompleteEvent` and omit it when a turn completes without an error.
- Preserve the full terminal `ErrorEvent` through the turn lifecycle so the completion event includes the same error details emitted separately by `EventMsg::Error`.
## Testing
- Extend the stream-error integration test to verify that `TurnCompleteEvent` contains the emitted error and that the next turn can still proceed.
GitOrigin-RevId: 8d32942a44132763076130102d8b89ab356d59d0
## What changed
- Add an optional `started_at` Unix timestamp to `TurnCompleteEvent` and
`TurnAbortedEvent`.
- Populate the timestamp from turn timing state when a turn completes or is
aborted.
- Preserve the timestamp when synthesizing interrupted fork history and when
importing external sessions.
## Testing
- Verify that interrupting a turn emits a `TurnAbortedEvent` with a start
timestamp.
GitOrigin-RevId: 75b3911b95c060e5fc1843a5e293d8fe47377186
## Why
Drive-shaped POSIX paths such as `/C:/workspace` can be mistaken for Windows
paths when an automatic approval request converts them from `PathUri`.
## What changed
- Keep approval paths as `PathUri` values together with their environment ID
until the automatic review request is built.
- Recover host-native paths for local working directories when strict
conversion cannot determine the path convention.
- Continue rejecting incompatible paths from remote environments.
## Testing
Add coverage for drive-shaped local POSIX paths, foreign remote paths, and
preserving apply-patch path URIs while building approval actions.
GitOrigin-RevId: 3fd31cfb4e8b46f9968ff66d5e986ca777430185
## What changed
- Run permission-request hooks before routing approvals to the automated reviewer or user, including when `strict_auto_review` is enabled.
- Centralize approval resolution so hook, automated-reviewer, and user decisions share rejection handling and report the correct telemetry source.
## Testing
- Add an integration test showing that an allow hook can approve a shell command during strict auto-review without invoking the automated reviewer.
GitOrigin-RevId: 18e9d76baee6cbae9d35ae517250f2f82270a9d2
## What changed
- Pass configured workspace roots from core to the exec server so filesystem and process sandbox permissions are materialized against the intended roots.
- Preserve an explicitly empty workspace-root list instead of treating the sandbox working directory as an implicit root.
- Initialize filesystem sandbox contexts with their working directory as the default workspace root.
## Testing
- Add end-to-end coverage for patch and command writes inside and outside workspace roots.
- Verify remote filesystem and process sandboxes do not grant access through an empty workspace-root list.
GitOrigin-RevId: 6684c8f7de50970b45a29e2a6323df954c96f9f6
## Why
Unified exec can drain process output multiple times while waiting for a command. Collecting those drains into an uncapped buffer allows large commands to exceed the output collection limit.
## What changed
- Accumulate drained output through the capped head/tail buffer so collection remains bounded while preserving the beginning and end of the stream.
- Include a `... N bytes omitted ...` marker when the collection cap drops output.
- Calculate the original token estimate from all observed bytes and preserve omission metadata through truncation and sandbox-denial responses.
## Testing
- Cover repeated drains, previously omitted output, omission markers, and large-output summaries.
GitOrigin-RevId: c6e2efeb875ed8f35914365eab7431f25fd3484c
## Why
Not every Codex distribution currently includes the
`codex-code-mode-host` companion binary. Enabling the process-host
feature should not make code mode unavailable on those surfaces while
packaging support is being completed.
## What changed
- Fall back to an in-process code-mode session only when spawning the
companion binary returns `io::ErrorKind::NotFound`.
- Keep permission, handshake, timeout, and other host failures visible
instead of silently falling back.
- Store the provider's owned-process/in-process choice as one
enum-backed state so later sessions reuse the fallback decision.
- Preserve the underlying spawn `io::Error` while retaining the host
path in the displayed error.
- Update provider, `CodeModeService`, and end-to-end coverage to verify
successful fallback execution.
## Test plan
- `just test -p codex-code-mode`
- `just test -p codex-core missing_process_host`
## Summary
Codex Apps file parameters are exposed to the model as local paths,
uploaded at execution time, and rewritten into provided-file payloads
before the MCP tool call.
The rewrite currently includes the documented optional fields
`mime_type` and `file_name` for every file parameter. Apps with strict
schemas can reject those fields when they are not declared.
## Changes
- Derive the supported optional file fields from each
`openai/fileParams` parameter's raw input schema before replacing it
with the model-visible local-path schema.
- Always include `download_url` and `file_id`.
- Include `mime_type` and `file_name` only when that specific file
parameter's schema accepts them, including schemas that allow additional
properties.
- Handle scalar and array file parameters, including items-only arrays,
composed schemas, and local JSON Schema references.
- Preserve the existing restriction that only the host-owned Codex Apps
MCP server can use this upload path.
This supports tools with different file contracts in the same app: one
parameter can accept the optional fields while another remains strict.
## Validation
- `just test -p codex-mcp`
- `just test -p codex-core mcp_openai_file`
- `just test -p codex-core codex_apps_file_params_`
- `just fix -p codex-mcp`
- `just fix -p codex-core`
- `just fmt`
- `git diff --check`
- Manually verified in the Codex Electron app that:
- a strict file schema receives only `download_url` and `file_id`
- a rich file schema also receives `mime_type` and `file_name`
Related: #31330
## Why
When MCP tools are deferred behind `tool_search`, Code mode keeps them
callable but omits their individual declarations from the initial `exec`
description. The shared MCP `CallToolResult` types were derived only
from directly rendered tools, so deferring every MCP tool also removed
the common response contract that models need to interpret MCP results.
This restores that contract without undoing the context savings from
deferred tool definitions. This is a follow-up to #29486.
## What changed
- Track deferred Code-mode tool definitions separately from directly
rendered definitions.
- Render the shared MCP type preamble when either direct or deferred MCP
tools are available.
- Keep deferred tool declarations out of the initial prompt.
- Add unit and integration coverage for deferred MCP tools.
## Testing
- `just test -p codex-code-mode-protocol`
- `just test -p codex-core
code_mode_only_guides_all_tools_search_and_calls_deferred_app_tools`
## Why
Auto-review performance is weaker because of confusing instructions
about sandbox permissions, and because it is given many tools which are
irrelevant to it.
## What
* Update the auto review prompt
* Remove the permissions_instructions developer message
* Only pass exec_tool and view_image tool to the reviewer
## Validation
`just fmt`
`cargo test -p codex-core --lib --quiet`
### summary
We want to pause code-mode from yielding back to the model when a
subcommand triggers an approval prompt. This means that all of these
previously inline blocking requests should also take out a
ElicitationService registration.
This also does some plumbing refactoring to request patch approval to
make it match the other `request_*_approval` methods in that it blocks
on the approval in the function instead of returning the oneshot
channel, this affords our ability to encapsulate the ElicitationService
registration via RAII.
Adds tests to confirm the blocking behavior for code_mode both in suite
tests and that the session holds them.
## Description
This PR removes the last path in core that emits `ExecCommandBegin` /
`ExecCommandEnd` directly.
Every command execution now starts and completes through canonical
`ItemStarted` / `ItemCompleted(TurnItem::CommandExecution)`. The
existing `HasLegacyEvent` compatibility layer still fans out Begin/End
afterward, so raw core event consumers and legacy rollout replay keep
seeing the same events.
`UnifiedExecInteraction` is dormant today. Live unified exec uses
`UnifiedExecStartup` for command lifecycle and `TerminalInteraction` for
`write_stdin` and polling, so this is code cleanup rather than a current
product behavior change. The main win is the code-level invariant where
all core flows emit `TurnItem` instead of legacy events.
## What changed
- Removed the `UnifiedExecInteraction` branches that emitted legacy
command events directly.
- Routed every command source through the existing canonical
`CommandExecution` lifecycle and compatibility fanout.
## Description
This PR migrates standalone web search onto the extension-owned
turn-item path introduced in #31283.
Standalone web search now emits `ExtensionItem::WebSearch` through
generic `TurnItem::Extension`, while app-server still exposes the
existing typed `ThreadItem::WebSearch` JSON shape. Hosted Responses API
web search stays on core-owned `TurnItem::WebSearch`.
## What changed
- Added `web_search::WebSearchItem` and `WebSearchAction` to
`codex-extension-items` under the stable `web.search` kind.
- Collapsed `ExtensionTurnItem` to generic `{ item, legacy_events }` now
that no typed extension special cases remain.
- Kept the existing `WebSearchBegin` / `WebSearchEnd` compatibility
events and canonical-first ordering.
- Updated app-server projection/history and generated TypeScript; the
app-server JSON schema is unchanged.
## Description
This PR adds a `codex-extension-items` crate for extension-owned
`TurnItem` schemas, and updates standalone image generation to start
using it via `TurnItem::Extension`.
This gives us a way to prevent Core from having to be aware of all
extension items. App-server still exposes the existing public
`ThreadItem::ImageGeneration` shape, now by wrapping the same shared
`image_generation::ImageGenerationItem` type.
The new `codex-extension-items` crate is necessary because the image gen
extension item is used by:
- `codex-image-generation-extension`, which produces it.
- `codex-tools / core`, which carry it generically.
- `codex-protocol`, which serializes it into lifecycle events and
rollouts.
- `app-server protocol`, which wraps it in public
`ThreadItem::ImageGeneration`
```
extension implementation
↓
codex-extension-items
↓
protocol / tools / app-server
```
We keep the hosted Responses API image generation as
`TurnItem::ImageGeneration` because core still owns its persistence and
legacy fanout.
### Before
Standalone image generation is implemented as an extension, but its item
representation previously lived in the core protocol. This sets the
precedent that core is aware of all extension items, which would be good
to avoid.
```
image-gen extension
→ constructs codex_protocol::ImageGenerationItem
→ emits ExtensionTurnItem::ImageGeneration
→ core matches ImageGeneration specially
→ protocol stores TurnItem::ImageGeneration
```
### After
```
image-gen extension
→ constructs extension-owned ImageGenerationItem
→ emits generic ExtensionItem
→ core transports/persists it generically
→ app-server wraps ImageGenerationItem as ThreadItem::ImageGeneration
```
Future extension items can have typed app-server APIs without adding a
new `TurnItem` variant, `ExtensionTurnItem` variant, or core emitter
match arm.
## What changed
- Added `codex-extension-items` with the closed `ExtensionItem` enum and
shared `image_generation::ImageGenerationItem` schema.
- Added generic `TurnItem::Extension(ExtensionItem)` and
`ExtensionTurnItem::Extension { item, legacy_events }` paths.
- Updated standalone image generation to emit a typed extension item and
provide its existing legacy `ImageGenerationBegin` /
`ImageGenerationEnd` events explicitly.
- Kept canonical lifecycle ordering: core emits `ItemStarted` /
`ItemCompleted` before extension-provided legacy events.
## Follow-up
Standalone web search still uses its typed special-case path. Migrating
it later would let `ExtensionTurnItem` collapse into a single
extension-item struct.
## Summary
- Rename the hosted web-search wire field to the canonical
`indexed_web_access` spelling.
- Preserve existing indexed-search behavior.
## Rollout
Merge and release only after server support for `indexed_web_access` is
fully deployed.
## Testing
- `just fmt`
- `just test -p codex-tools
web_search_tool_spec_serializes_expected_wire_shape`
- Blocking CI passed, including indexed web-search integration coverage.
This PR depends on [#31296](https://github.com/openai/codex/pull/31296)
for the canonical-to-legacy event mappings.
## Description
This PR makes the v1 and v2 wait paths emit canonical
`TurnItem::CollabAgentToolCall` lifecycle instead of
`CollabWaitingBegin` / `CollabWaitingEnd` directly.
Both paths already used the same legacy waiting events before this PR.
The v1 item carries receiver metadata and final agent statuses for its
target agents; v2 waits for mailbox activity rather than specific
agents, so it keeps those fields empty, matching the existing v2 legacy
payload.
App-server v2 consumes the canonical item directly and ignores the
mapped legacy wait events.
## Why
Wait is separate from the other collab tools because it is multi-target
and has distinct timeout/status behavior. Keeping it last also lets this
PR remove the old helper that only existed to shape legacy wait status
entries in core.
## What changed
- Emit canonical collab wait items from both v1 and v2 wait handlers.
- Preserve receiver metadata and agent status snapshots on completed
wait items.
- Remove the old core helper for building legacy wait status entries.
## Follow-up
The next stack PR, [#30188](https://github.com/openai/codex/pull/30188),
writes canonical `TurnItem` values to paginated rollout files.
This PR depends on [#31296](https://github.com/openai/codex/pull/31296)
for the canonical-to-legacy event mappings.
## Description
This PR makes the non-wait v1 collaboration tools—spawn, send input,
resume, and close—emit canonical `TurnItem::CollabAgentToolCall`
lifecycle instead of their legacy begin/end events directly.
App-server v2 consumes the canonical collab items directly, ignores the
mapped legacy events, and applies close-agent thread-watch cleanup from
the completed item.
## Why
These four tools share the same single-target lifecycle shape. Wait
stays separate because it carries multi-target status snapshots and has
its own status-shaping cleanup.
## What changed
- Add shared helpers for emitting canonical collab tool-call lifecycle.
- Migrate spawn, send input, resume, and close handlers.
- Move close-agent watcher cleanup onto canonical completed collab
items.
This PR depends on [#31296](https://github.com/openai/codex/pull/31296)
for the canonical-to-legacy event mappings.
## Description
This PR makes the MultiAgentV2 spawn, message/follow-up, and interrupt
paths emit completed canonical `TurnItem::SubAgentActivity` items
instead of `SubAgentActivityEvent` directly.
App-server v2 now applies interrupted-agent thread-watch cleanup from
the canonical completed item and ignores the mapped legacy activity
event.
## Why
Sub-agent activity is separate from the v1 collab tool begin/end
lifecycle. Keeping it separate makes the v2 watcher side effect
reviewable without mixing in the larger collab tool-call migration.
## What changed
- Emit canonical sub-agent activity items from v2 spawn,
message/follow-up, and interrupt paths.
- Move missing-thread watcher cleanup onto canonical completed activity
items.
- Update focused app-server coverage to exercise canonical interrupted
activity.
This PR depends on [#31296](https://github.com/openai/codex/pull/31296)
for the canonical-to-legacy event mappings.
## Description
This PR makes dynamic tools emit canonical `TurnItem::DynamicToolCall`
lifecycle instead of `DynamicToolCallRequest` /
`DynamicToolCallResponse` directly.
App-server v2 now sends the client `DynamicToolCall` request from the
canonical item start. It ignores the mapped legacy request/response
events, so clients receive one item start and one tool request.
## Why
Dynamic tools are a separate migration boundary because their start
event also drives a client request. Keeping that routing change isolated
makes it easier to verify that the request still happens exactly once.
## What changed
- Emit in-progress and completed/failed dynamic tool items from the
dynamic tool handler.
- Move app-server client request dispatch onto canonical dynamic item
starts.
- Add focused app-server coverage for the canonical start notification
and client request.
This PR depends on [#31296](https://github.com/openai/codex/pull/31296)
for the canonical-to-legacy event mappings.
## Description
This PR makes command execution emit canonical
`TurnItem::CommandExecution` lifecycle from both the shell tool path and
user `/shell` commands.
App-server v2 consumes the canonical command items directly and ignores
the mapped `ExecCommandBegin` / `ExecCommandEnd` compatibility events,
so clients still receive one command item lifecycle.
`UnifiedExecInteraction` stays on the legacy path because
`TerminalInteraction` is still the v2 surface for stdin and poll events.
Emitting a command item there would render the same wait twice.
## Why
This is the first live producer migration after the compatibility
mappings in #31296. Keeping command execution separate makes the unified
exec exception reviewable without mixing in dynamic tools or multi-agent
behavior.
## What changed
- Emit canonical command execution items from shell tool events and user
shell commands.
- Preserve the existing unified exec interaction carveout.
- Move app-server command deduplication and completion bookkeeping onto
canonical item events.
- Update unified exec coverage to assert the completed command item.
## Why
Managed-network commands within one Codex conversation share the same
HTTP and SOCKS proxy ingress. When several exec calls run concurrently,
the proxy sees the requested destination but cannot tell which exec
opened the connection.
For example:
```text
exec A: curl https://example.com/a ─┐
├─> conversation proxy ─> Guardian
exec B: curl https://example.com/b ─┘ host: example.com
trigger: unknown
```. Three parallel network execs reached Guardian without their
triggering call IDs or commands. Guardian denied the requests, but Codex
could not safely associate those outcomes with the individual tool
calls.
## What changes
Keep the shared proxy ingress and tag each connection at the existing
trusted Linux bridge:
```text
exec A ─> existing Linux bridge ─> [token A][proxy bytes] ─┐
├─> shared HTTP/SOCKS ingress
exec B ─> existing Linux bridge ─> [token B][proxy bytes] ─┘
│
token A ─> exec A ─────┤
token B ─> exec B ─────┘
```
The complete path is:
```text
active exec registration
│
├─ registers its UUID as a short-lived attribution token
├─ passes the token to the Linux sandbox helper
├─ helper removes the token before launching the user command
├─ existing host bridge prepends the token to each proxy connection
├─ shared proxy consumes the bounded attribution frame
└─ proxy attaches the matching execution-scoped state
├─ Guardian receives the exact call ID and command
└─ a denial finishes/cancels the matching tool call
```
Dropping the active or deferred exec registration removes the token.
Connections that were already accepted retain their resolved
attribution; new connections using an expired token fail closed.
## Before and after
Before, Guardian could receive only the network destination:
```json
{
"tool": "network_access",
"host": "www.17track.net",
"port": 443,
"protocol": "https"
}
```
After, the same request includes the action that caused it:
```json
{
"tool": "network_access",
"host": "www.17track.net",
"port": 443,
"protocol": "https",
"trigger": {
"callId": "exec-network-first",
"command": ["/bin/sh", "-c", "curl https://www.17track.net"]
}
}
```
## Listener accounting
This PR does **not** create proxy listeners per exec.
```text
Existing topology:
one conversation -> one HTTP listener + optional one SOCKS listener
Discarded per-exec approach:
one conversation -> existing listener pair
+ up to one additional listener pair per active exec
This PR:
one conversation -> existing listener pair only
+ one small token-map entry per active exec
```
The Linux sandbox already creates a trusted routing bridge for each
sandboxed command. This PR adds a short frame write to that bridge
rather than introducing another listener, task, or proxy process.
The existing conversation-scoped listener pair remains. Making a single
proxy service shared across multiple conversations would be a separate
multi-tenant architecture change involving per-conversation policy,
configuration, audit, and Guardian routing.
## Keeping the implementation small
The attribution is bound once, when the TCP connection enters the proxy.
The ingress installs an execution-scoped clone of the existing
`NetworkProxyState`, so the established HTTP, SOCKS, MITM, policy,
audit, and blocked-request paths continue using their existing state
lookup.
This avoids plumbing a new request-context type through every protocol
handler. Outside the two ingress wrappers, protocol-specific request
handling is unchanged.
## Security behavior
- Tokens are generated from the existing random execution registration
IDs.
- The trusted Linux helper consumes and removes the token before
executing user code.
- Attribution frames have a fixed magic prefix, bounded token length,
and bounded read timeout.
- Unknown or expired tokens close the connection.
- A token presented to a proxy for another environment closes the
connection.
- Existing unframed callers preserve the current conservative
attribution behavior.
## Platform scope
Exact bridge attribution is enabled on Linux. macOS and Windows retain
their current shared-proxy behavior.
## Test coverage
The concurrent end-to-end test starts two managed-network execs together
and synchronizes them so both are active before either connects. It then
inspects the two Guardian requests and compares the complete attribution
pairs:
```text
(exec-network-first, exact first command)
(exec-network-second, exact second command)
```
Focused proxy coverage verifies the bounded frame and that a registered
framed connection receives the matching execution and environment state.
## Scope
This fixes the Linux network-to-exec attribution path and records a
denial against the exact matching tool call. It intentionally does not
change:
- delivery of an entirely unattributed denial to the parent turn;
- how parallel denials count toward the Guardian circuit breaker;
- how the UI displays the rejection reason or completed-turn state.
Those remain separate concerns from attribution.
## Relationship to #29456 and #29668#29456 made the proxy environment and sandbox policy come from the same
prepared network context. This PR adds the execution token to that
prepared launch and consumes it at the shared ingress.
This follows #29668's shared-ingress framing direction, but completes
the production registration, Linux bridge, core call mapping, denial
mapping, and concurrent end-to-end path. It also keeps attribution in
the existing per-connection proxy state instead of introducing
request-context plumbing through every HTTP, SOCKS, and MITM handler.
This PR is intended to supersede #29668 for the Linux attribution fix.
---------
Co-authored-by: viyatb-oai <viyatb@openai.com>
Co-authored-by: Codex <noreply@openai.com>
## Summary
As part of our effort to start simplifying approvals code, this PR
extracts Guardian approvals logic from shell tool calls, and replaces it
with the ApprovalAction abstraction instead. This way, tools don't need
to know about Guardian at all.
## Testing
- [x] Adds integration test
## Why
Code-mode tool results could return to the model while an MCP
elicitation was still waiting for user input. This differed from
parallel tool calling and could let the model continue before the user
resolved the request.
We need one session-level view of outstanding elicitations so tool
runtimes can consistently hold results until every pending elicitation
is resolved.
## What changed
- Added a counted, session-owned ElicitationService with RAII
registrations.
- Registered both core-originated and server-originated MCP elicitations
with the service.
- Migrated out-of-band elicitation tracking and unified exec timeout
pausing to the shared service.
- Made code-mode functions.exec and functions.wait capture their runtime
result normally, then hold it before returning while an elicitation is
outstanding.
- Kept terminate: true immediate; only its result is held.
- Preserved model-visible wall time across the elicitation hold.
- Kept the behavior session-scoped, with concurrent elicitations holding
the pause until all registrations are released.
## Why
App-server deployments can consume structured JSON logs for operational
measurements without requiring an OTEL exporter. Existing tool-result
telemetry reports the handler outcome, but it does not separate time
spent waiting to dispatch from time spent executing the handler.
A compact completion event for the outer, direct tool call lets
consumers measure those phases and correlate them with a conversation
and turn. Code-mode calls are intentionally excluded so nested runtime
calls do not create overlapping events that are easy to double-count.
## What changed
- Added a
[`ToolCallTimingGuard`](https://github.com/openai/codex/blob/141110a73cff041c42fd51f47e363e83df1dde51/codex-rs/core/src/tools/parallel.rs#L32)
around direct tool calls. Event-only strings and timing state are
captured only when the `codex_core::tools::parallel` `INFO` target is
enabled.
- Added a
[`codex.tool_call`](https://github.com/openai/codex/blob/141110a73cff041c42fd51f47e363e83df1dde51/codex-rs/core/src/tools/parallel.rs#L313)
completion event with conversation, turn, tool, call, trace, dispatch,
handler, and total timing fields.
- Recorded the execution-start marker after the dispatch lock is
acquired. Event emission snapshots that marker once so a concurrently
starting dispatch cannot produce internally inconsistent fields.
- Limited the event to `ToolCallSource::Direct`; [unit
coverage](https://github.com/openai/codex/blob/141110a73cff041c42fd51f47e363e83df1dde51/codex-rs/core/src/tools/parallel.rs#L365)
verifies code-mode calls are ignored.
- Added [cancellation
coverage](https://github.com/openai/codex/blob/141110a73cff041c42fd51f47e363e83df1dde51/codex-rs/core/src/tools/parallel.rs#L408)
that holds the execution gate and verifies a call cancelled before
admission emits exactly one dispatch-only timing event.
- Added reusable
[`JsonLogCapture`](https://github.com/openai/codex/blob/141110a73cff041c42fd51f47e363e83df1dde51/codex-rs/app-server/tests/common/json_logging.rs#L15)
support, a [JSON-logging-specific `TestAppServer`
constructor](https://github.com/openai/codex/blob/141110a73cff041c42fd51f47e363e83df1dde51/codex-rs/app-server/tests/common/test_app_server.rs#L172),
and an [end-to-end app-server
test](https://github.com/openai/codex/blob/141110a73cff041c42fd51f47e363e83df1dde51/codex-rs/app-server/tests/suite/logging.rs#L52)
that drives a direct `exec_command` through the public v2 JSON-RPC API
and validates the emitted JSON event.
Exec-server-specific request and process timing remains in the stacked
PR #30901.
## Suggested logging filter
```bash
LOG_FORMAT=json \
RUST_LOG='warn,codex_core::tools::parallel=info' \
codex app-server
```
## Event example
Identifier and timing values are illustrative.
### `codex.tool_call`
```json
{
"timestamp": "2026-06-27T03:45:20.443Z",
"level": "INFO",
"fields": {
"message": "tool call completed",
"event.name": "codex.tool_call",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"conversation.id": "67e55044-10b1-426f-9247-bb680e5fe0c8",
"turn_id": "019f04f8-6ac2-78f1-8625-f04a6d35af18",
"tool_name": "exec_command",
"call_id": "call_7b8483",
"tool_source": "direct",
"execution_started": true,
"dispatch_duration_ms": 12,
"handler_duration_ms": 431,
"total_duration_ms": 443
},
"target": "codex_core::tools::parallel"
}
```
If execution never starts, `execution_started` is `false`,
`handler_duration_ms` is `0`, and `dispatch_duration_ms` covers the full
observed lifetime.
If a duration cannot be represented as an unsigned 64-bit millisecond
value, all three duration fields are omitted rather than populated with
a sentinel that could corrupt downstream calculations.
## Test plan
- `just test -p codex-core
tool_call_timing_guard_ignores_code_mode_source`
- `just test -p codex-core
cancellation_before_dispatch_admission_logs_dispatch_only_timing`
- `just test -p codex-app-server
app_server_emits_structured_tool_call_timing_event`
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/30334).
* __->__ #30334