## Why
`PermissionProfile` should only describe the per-command permissions we
still want to grant dynamically. Keeping
`MacOsSeatbeltProfileExtensions` in that surface forced extra macOS-only
approval, protocol, schema, and TUI branches for a capability we no
longer want to expose.
## What changed
- Removed the macOS-specific permission-profile types from
`codex-protocol`, the app-server v2 API, and the generated
schema/TypeScript artifacts.
- Deleted the core and sandboxing plumbing that threaded
`MacOsSeatbeltProfileExtensions` through execution requests and seatbelt
construction.
- Simplified macOS seatbelt generation so it always includes the fixed
read-only preferences allowlist instead of carrying a configurable
profile extension.
- Removed the macOS additional-permissions UI/docs/test coverage and
deleted the obsolete macOS permission modules.
- Tightened `request_permissions` intersection handling so explicitly
empty requested read lists are preserved only when that field was
actually granted, avoiding zero-grant responses being stored as active
permissions.
## Why
Skill metadata accepted a `permissions` block and stored the result on
`SkillMetadata`, but that data was never consumed by runtime behavior.
Leaving the dead parsing path in place makes it look like skills can
widen or otherwise influence execution permissions when, in practice,
declared skill permissions are ignored.
This change removes that misleading surface area so the skill metadata
model matches what the system actually uses.
## What changed
- removed `permission_profile` and `managed_network_override` from
`core-skills::SkillMetadata`
- stopped parsing `permissions` from skill metadata in
`core-skills/src/loader.rs`
- deleted the loader tests that only exercised the removed permissions
parsing path
- cleaned up dependent `SkillMetadata` constructors in tests and TUI
code that were only carrying `None` for those fields
## Testing
- `cargo test -p codex-core-skills`
- `cargo test -p codex-tui
submission_prefers_selected_duplicate_skill_path`
- `just argument-comment-lint`
- Removes provenance filtering in the mentions feature for apps and
skills that were installed as part of a plugin.
- All skills and apps for a plugin are mentionable with this change.
- Remove numeric prefixes for disabled rows in shared list rendering.
These numbers are shortcuts, Ex: Pressing "2" selects option `#2`.
Disabled items can not be selected, so keeping numbers on these items is
misleading.
- Apply the same behavior in both tui and tui_app_server.
- Update affected snapshots for apps/plugins loading and plugin detail
rows.
_**This is a global change.**_
Before:
<img width="1680" height="488" alt="image"
src="https://github.com/user-attachments/assets/4bcf94ad-285f-48d3-a235-a85b58ee58e2"
/>
After:
<img width="1706" height="484" alt="image"
src="https://github.com/user-attachments/assets/76bb6107-a562-42fe-ae94-29440447ca77"
/>
- Prefer plugin manifest `interface.displayName` for plugin labels.
- Preserve plugin provenance when handling `list_mcp_tools` so connector
`plugin_display_names` are not clobbered.
- Add a TUI test to ensure plugin-owned app mentions are deduped
correctly.
## Summary
- queue input after the user submits `/compact` until that manual
compact turn ends
- mirror the same behavior in the app-server TUI
- add regressions for input queued before compact starts and while it is
running
Co-authored-by: Codex <noreply@openai.com>
- Duplicate app mentions are now suppressed when they’re plugin-backed
with the same display name.
- Remaining connector mentions now label category as [Plugin] when
plugin metadata is present, otherwise [App].
- Mention result lists are now capped to 8 rows after filtering.
- Updates both tui and tui_app_server with the same changes.
## Summary
- use Shift+Left to edit the most recent queued message when running
under tmux
- mirror the same binding change in the app-server TUI
- add tmux-specific tests and snapshot coverage for the rendered
queued-message hint
## Testing
- just fmt
- cargo test -p codex-tui
- cargo test -p codex-tui-app-server
- just argument-comment-lint -p codex-tui -p codex-tui-app-server
Co-authored-by: Codex <noreply@openai.com>
- Split the feature system into a new `codex-features` crate.
- Cut `codex-core` and workspace consumers over to the new config and
warning APIs.
Co-authored-by: Ahmed Ibrahim <219906144+aibrahim-oai@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
## Problem
When multiple Codex sessions are open at once, terminal tabs and windows
are hard to distinguish from each other. The existing status line only
helps once the TUI is already focused, so it does not solve the "which
tab is this?" problem.
This PR adds a first-class `/title` command so the terminal window or
tab title can carry a short, configurable summary of the current
session.
## Screenshot
<img width="849" height="320" alt="image"
src="https://github.com/user-attachments/assets/8b112927-7890-45ed-bb1e-adf2f584663d"
/>
## Mental model
`/statusline` and `/title` are separate status surfaces with different
constraints. The status line is an in-app footer that can be denser and
more detailed. The terminal title is external terminal metadata, so it
needs short, stable segments that still make multiple sessions easy to
tell apart.
The `/title` configuration is an ordered list of compact items. By
default it renders `spinner,project`, so active sessions show
lightweight progress first while idle sessions still stay easy to
disambiguate. Each configured item is omitted when its value is not
currently available rather than forcing a placeholder.
## Non-goals
This does not merge `/title` into `/statusline`, and it does not add an
arbitrary free-form title string. The feature is intentionally limited
to a small set of structured items so the title stays short and
reviewable.
This also does not attempt to restore whatever title the terminal or
shell had before Codex started. When Codex clears the title, it clears
the title Codex last wrote.
## Tradeoffs
A separate `/title` command adds some conceptual overlap with
`/statusline`, but it keeps title-specific constraints explicit instead
of forcing the status line model to cover two different surfaces.
Title refresh can happen frequently, so the implementation now shares
parsing and git-branch orchestration between the status line and title
paths, and caches the derived project-root name by cwd. That keeps the
hot path cheap without introducing background polling.
## Architecture
The TUI gets a new `/title` slash command and a dedicated picker UI for
selecting and ordering terminal-title items. The chosen ids are
persisted in `tui.terminal_title`, with `spinner` and `project` as the
default when the config is unset. `status` remains available as a
separate text item, so configurations like `spinner,status` render
compact progress like `⠋ Working`.
`ChatWidget` now refreshes both status surfaces through a shared
`refresh_status_surfaces()` path. That shared path parses configured
items once, warns on invalid ids once, synchronizes shared cached state
such as git-branch lookup, then renders the footer status line and
terminal title from the same snapshot.
Low-level OSC title writes live in `codex-rs/tui/src/terminal_title.rs`,
which owns the terminal write path and last-mile sanitization before
emitting OSC 0.
## Security
Terminal-title text is treated as untrusted display content before Codex
emits it. The write path strips control characters, removes invisible
and bidi formatting characters that can make the title visually
misleading, normalizes whitespace, and caps the emitted length.
References used while implementing this:
- [xterm control
sequences](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
- [WezTerm escape sequences](https://wezterm.org/escape-sequences.html)
- [CWE-150: Improper Neutralization of Escape, Meta, or Control
Sequences](https://cwe.mitre.org/data/definitions/150.html)
- [CERT VU#999008 (Trojan Source)](https://kb.cert.org/vuls/id/999008)
- [Trojan Source disclosure site](https://trojansource.codes/)
- [Unicode Bidirectional Algorithm (UAX
#9)](https://www.unicode.org/reports/tr9/)
- [Unicode Security Considerations (UTR
#36)](https://www.unicode.org/reports/tr36/)
## Observability
Unknown configured title item ids are warned about once instead of
repeatedly spamming the transcript. Live preview applies immediately
while the `/title` picker is open, and cancel rolls the in-memory title
selection back to the pre-picker value.
If terminal title writes fail, the TUI emits debug logs around set and
clear attempts. The rendered status label intentionally collapses richer
internal states into compact title text such as `Starting...`, `Ready`,
`Thinking...`, `Working...`, `Waiting...`, and `Undoing...` when
`status` is configured.
## Tests
Ran:
- `just fmt`
- `cargo test -p codex-tui`
At the moment, the red Windows `rust-ci` failures are due to existing
`codex-core` `apply_patch_cli` stack-overflow tests that also reproduce
on `main`. The `/title`-specific `codex-tui` suite is green.
It now supports:
- Connectors that are from installed and enabled plugins that are not
installed yet
- Plugins that are on the allowlist that are not installed yet.
## Why
Once the repo-local lint exists, `codex-rs` needs to follow the
checked-in convention and CI needs to keep it from drifting. This commit
applies the fallback `/*param*/` style consistently across existing
positional literal call sites without changing those APIs.
The longer-term preference is still to avoid APIs that require comments
by choosing clearer parameter types and call shapes. This PR is
intentionally the mechanical follow-through for the places where the
existing signatures stay in place.
After rebasing onto newer `main`, the rollout also had to cover newly
introduced `tui_app_server` call sites. That made it clear the first cut
of the CI job was too expensive for the common path: it was spending
almost as much time installing `cargo-dylint` and re-testing the lint
crate as a representative test job spends running product tests. The CI
update keeps the full workspace enforcement but trims that extra
overhead from ordinary `codex-rs` PRs.
## What changed
- keep a dedicated `argument_comment_lint` job in `rust-ci`
- mechanically annotate remaining opaque positional literals across
`codex-rs` with exact `/*param*/` comments, including the rebased
`tui_app_server` call sites that now fall under the lint
- keep the checked-in style aligned with the lint policy by using
`/*param*/` and leaving string and char literals uncommented
- cache `cargo-dylint`, `dylint-link`, and the relevant Cargo
registry/git metadata in the lint job
- split changed-path detection so the lint crate's own `cargo test` step
runs only when `tools/argument-comment-lint/*` or `rust-ci.yml` changes
- continue to run the repo wrapper over the `codex-rs` workspace, so
product-code enforcement is unchanged
Most of the code changes in this commit are intentionally mechanical
comment rewrites or insertions driven by the lint itself.
## Verification
- `./tools/argument-comment-lint/run.sh --workspace`
- `cargo test -p codex-tui-app-server -p codex-tui`
- parsed `.github/workflows/rust-ci.yml` locally with PyYAML
---
* -> #14652
* #14651
### Motivation
- Interrupting a running turn (Ctrl+C / Esc) currently also terminates
long‑running background shells, which is surprising for workflows like
local dev servers or file watchers.
- The existing cleanup command name was confusing; callers expect an
explicit command to stop background terminals rather than a UI clear
action.
- Make background‑shell termination explicit and surface a clearer
command name while preserving backward compatibility.
### Description
- Renamed the background‑terminal cleanup slash command from `Clean`
(`/clean`) to `Stop` (`/stop`) and kept `clean` as an alias in the
command parsing/visibility layer, updated the user descriptions and
command popup wiring accordingly.
- Updated the unified‑exec footer text and snapshots to point to `/stop`
(and trimmed corresponding snapshot output to match the new label).
- Changed interrupt behavior so `Op::Interrupt` (Ctrl+C / Esc interrupt)
no longer closes or clears tracked unified exec / background terminal
processes in the TUI or core cleanup path; background shells are now
preserved after an interrupt.
- Updated protocol/docs to clarify that `turn/interrupt` (or
`Op::Interrupt`) interrupts the active turn but does not terminate
background terminals, and that `thread/backgroundTerminals/clean` is the
explicit API to stop those shells.
- Updated unit/integration tests and insta snapshots in the TUI and core
unified‑exec suites to reflect the new semantics and command name.
### Testing
- Ran formatting with `just fmt` in `codex-rs` (succeeded).
- Ran `cargo test -p codex-protocol` (succeeded).
- Attempted `cargo test -p codex-tui` but the build could not complete
in this environment due to a native build dependency that requires
`libcap` development headers (the `codex-linux-sandbox` vendored build
step); install `libcap-dev` / make `libcap.pc` available in
`PKG_CONFIG_PATH` to run the TUI test suite locally.
- Updated and accepted the affected `insta` snapshots for the TUI
changes so visual diffs reflect the new `/stop` wording and preserved
interrupt behavior.
------
[Codex
Task](https://chatgpt.com/codex/tasks/task_i_69b39c44b6dc8323bd133ae206310fae)
## Summary
- add `approvals_reviewer = "user" | "guardian_subagent"` as the runtime
control for who reviews approval requests
- route Smart Approvals guardian review through core for command
execution, file changes, managed-network approvals, MCP approvals, and
delegated/subagent approval flows
- expose guardian review in app-server with temporary unstable
`item/autoApprovalReview/{started,completed}` notifications carrying
`targetItemId`, `review`, and `action`
- update the TUI so Smart Approvals can be enabled from `/experimental`,
aligned with the matching `/approvals` mode, and surfaced clearly while
reviews are pending or resolved
## Runtime model
This PR does not introduce a new `approval_policy`.
Instead:
- `approval_policy` still controls when approval is needed
- `approvals_reviewer` controls who reviewable approval requests are
routed to:
- `user`
- `guardian_subagent`
`guardian_subagent` is a carefully prompted reviewer subagent that
gathers relevant context and applies a risk-based decision framework
before approving or denying the request.
The `smart_approvals` feature flag is a rollout/UI gate. Core runtime
behavior keys off `approvals_reviewer`.
When Smart Approvals is enabled from the TUI, it also switches the
current `/approvals` settings to the matching Smart Approvals mode so
users immediately see guardian review in the active thread:
- `approval_policy = on-request`
- `approvals_reviewer = guardian_subagent`
- `sandbox_mode = workspace-write`
Users can still change `/approvals` afterward.
Config-load behavior stays intentionally narrow:
- plain `smart_approvals = true` in `config.toml` remains just the
rollout/UI gate and does not auto-set `approvals_reviewer`
- the deprecated `guardian_approval = true` alias migration does
backfill `approvals_reviewer = "guardian_subagent"` in the same scope
when that reviewer is not already configured there, so old configs
preserve their original guardian-enabled behavior
ARC remains a separate safety check. For MCP tool approvals, ARC
escalations now flow into the configured reviewer instead of always
bypassing guardian and forcing manual review.
## Config stability
The runtime reviewer override is stable, but the config-backed
app-server protocol shape is still settling.
- `thread/start`, `thread/resume`, and `turn/start` keep stable
`approvalsReviewer` overrides
- the config-backed `approvals_reviewer` exposure returned via
`config/read` (including profile-level config) is now marked
`[UNSTABLE]` / experimental in the app-server protocol until we are more
confident in that config surface
## App-server surface
This PR intentionally keeps the guardian app-server shape narrow and
temporary.
It adds generic unstable lifecycle notifications:
- `item/autoApprovalReview/started`
- `item/autoApprovalReview/completed`
with payloads of the form:
- `{ threadId, turnId, targetItemId, review, action? }`
`review` is currently:
- `{ status, riskScore?, riskLevel?, rationale? }`
- where `status` is one of `inProgress`, `approved`, `denied`, or
`aborted`
`action` carries the guardian action summary payload from core when
available. This lets clients render temporary standalone pending-review
UI, including parallel reviews, even when the underlying tool item has
not been emitted yet.
These notifications are explicitly documented as `[UNSTABLE]` and
expected to change soon.
This PR does **not** persist guardian review state onto `thread/read`
tool items. The intended follow-up is to attach guardian review state to
the reviewed tool item lifecycle instead, which would improve
consistency with manual approvals and allow thread history / reconnect
flows to replay guardian review state directly.
## TUI behavior
- `/experimental` exposes the rollout gate as `Smart Approvals`
- enabling it in the TUI enables the feature and switches the current
session to the matching Smart Approvals `/approvals` mode
- disabling it in the TUI clears the persisted `approvals_reviewer`
override when appropriate and returns the session to default manual
review when the effective reviewer changes
- `/approvals` still exposes the reviewer choice directly
- the TUI renders:
- pending guardian review state in the live status footer, including
parallel review aggregation
- resolved approval/denial state in history
## Scope notes
This PR includes the supporting core/runtime work needed to make Smart
Approvals usable end-to-end:
- shell / unified-exec / apply_patch / managed-network / MCP guardian
review
- delegated/subagent approval routing into guardian review
- guardian review risk metadata and action summaries for app-server/TUI
- config/profile/TUI handling for `smart_approvals`, `guardian_approval`
alias migration, and `approvals_reviewer`
- a small internal cleanup of delegated approval forwarding to dedupe
fallback paths and simplify guardian-vs-parent approval waiting (no
intended behavior change)
Out of scope for this PR:
- redesigning the existing manual approval protocol shapes
- persisting guardian review state onto app-server `ThreadItem`s
- delegated MCP elicitation auto-review (the current delegated MCP
guardian shim only covers the legacy `RequestUserInput` path)
---------
Co-authored-by: Codex <noreply@openai.com>
This PR changes app and connector enablement when `requirements.toml` is
present locally or via remote configuration.
For apps.* entries:
- `enabled = false` in `requirements.toml` overrides the user’s local
`config.toml` and forces the app to be disabled.
- `enabled = true` in `requirements.toml` does not re-enable an app the
user has disabled in config.toml.
This behavior applies whether or not the user has an explicit entry for
that app in `config.toml`. It also applies to cloud-managed policies and
configurations when the admin sets the override through
`requirements.toml`.
Scenarios tested and verified:
- Remote managed, user config (present) override
- Admin-defined policies & configurations include a connector override:
`[apps.<appID>]
enabled = false`
- User's config.toml has the same connector configured with `enabled =
true`
- TUI/App should show connector as disabled
- Connector should be unavailable for use in the composer
- Remote managed, user config (absent) override
- Admin-defined policies & configurations include a connector override:
`[apps.<appID>]
enabled = false`
- User's config.toml has no entry for the the same connector
- TUI/App should show connector as disabled
- Connector should be unavailable for use in the composer
- Locally managed, user config (present) override
- Local requirements.toml includes a connector override:
`[apps.<appID>]
enabled = false`
- User's config.toml has the same connector configured with `enabled =
true`
- TUI/App should show connector as disabled
- Connector should be unavailable for use in the composer
- Locally managed, user config (absent) override
- Local requirements.toml includes a connector override:
`[apps.<appID>]
enabled = false`
- User's config.toml has no entry for the the same connector
- TUI/App should show connector as disabled
- Connector should be unavailable for use in the composer
<img width="1446" height="753" alt="image"
src="https://github.com/user-attachments/assets/61c714ca-dcca-4952-8ad2-0afc16ff3835"
/>
<img width="595" height="233" alt="image"
src="https://github.com/user-attachments/assets/7c8ab147-8fd7-429a-89fb-591c21c15621"
/>
## Summary
This lets skill loading split `permissions.network` into two distinct
pieces:
- `permissions.network.enabled` still feeds the skill
`PermissionProfile` and remains the coarse gate for whether the skill
can use network access at all.
- `permissions.network.allowed_domains` and
`permissions.network.denied_domains` are lifted into a new
`SkillManagedNetworkOverride` so managed-network sessions can start
per-skill scoped proxies with the right domain overrides.
The change also updates `SkillMetadata` construction sites and adds
loader tests covering YAML parsing plus normalization of the network
gate vs. domain override fields.
## Follow-up
A PR that uses the network_override to spin up a skill-specific proxy if
network_override is not none.
## Summary
- add ARC monitor support for MCP tool calls by serializing MCP approval
requests into the ARC action shape and sending the relevant
conversation/policy context to the `/api/codex/safety/arc` endpoint
- route ARC outcomes back into MCP approval flow so `ask-user` falls
back to a user prompt and `steer-model` blocks the tool call, with
guardian/ARC tests covering the new request shape
- update the TUI approval copy from “Approve Once” to “Allow” / “Allow
for this session” and refresh the related
snapshots
---------
Co-authored-by: Fouad Matin <fouad@openai.com>
Co-authored-by: Fouad Matin <169186268+fouad-openai@users.noreply.github.com>
#### What
###### Context + Problem
With the introduction of plugins, we now have one more type of
`$`-mentionable item in the TUI's popup menu on `$`. Apps, skills, and
plugins can all have the same user-facing name, and we attempt to
distinguish with a category tag suffix, like `[App]`. This has a few
problems:
- We decide to show tags by the text that will be inserted into the
conversation, not the actual user-visible text, so two visibly-identical
entries can have no clarifying category tag suffix
- The category tag is a suffix and commonly gets cut off by long
descriptions
- The skill category tag is currently only displayed on repo skills as
`[Repo]`, which is confusing to most users
- The plugin category tag is currently `[<marketplace-name>]`, which is
also confusing to most users
###### Solution
- **Always** show a **prefix** category tag that is `[Skill]`, `[App]`,
or `[Plugin]`. No conditional rendering or copy.
Before:
<img width="801" height="153" alt="image"
src="https://github.com/user-attachments/assets/448e06e7-2af8-4c14-9804-ed1ca17cf514"
/>
After:
<img width="800" height="118" alt="image"
src="https://github.com/user-attachments/assets/57895b41-06fe-4d92-887b-68704c5a15fd"
/>
I also feel this clarifies the results at-a-glance while you scroll:
https://github.com/user-attachments/assets/cbdd5840-53d9-4656-812c-6e816755e1fd
### Tests
Added + updated tests (including snapshots), tested locally
## Summary
request_permissions flows should support persisting results for the
session.
Open Question: Still deciding if we need within-turn approvals - this
adds complexity but I could see it being useful
## Testing
- [x] Updated unit tests
---------
Co-authored-by: Codex <noreply@openai.com>
## Summary
- split the pending input preview into labeled pending-steer and queued
follow-up sections
- explain that pending steers submit after the next tool call and that
Esc can interrupt and send them immediately
- treat Esc as an interrupt-plus-resubmit path when pending steers
exist, with updated TUI snapshots and tests
Queues and steers:
<img width="1038" height="263" alt="Screenshot 2026-03-07 at 10 17
17 PM"
src="https://github.com/user-attachments/assets/4ef433ef-27a3-4b7c-ad69-2046f6eb89e6"
/>
After pressing Esc:
<img width="1046" height="320" alt="Screenshot 2026-03-07 at 10 17
21 PM"
src="https://github.com/user-attachments/assets/0f4d89e0-b6b9-486a-9f04-b6021f169ba7"
/>
## Codex author
`codex resume 019cc6f4-2cca-7803-b717-8264526dbd97`
---------
Co-authored-by: Codex <noreply@openai.com>
Addresses feature request #13660
Adds new option to `/statusline` so the status line can display "fast
on" or "fast off"
Summary
- introduce a `FastMode` status-line item so `/statusline` can render
explicit `Fast on`/`Fast off` text for the service tier
- wire the item into the picker metadata and resolve its string from
`ChatWidget` without adding any unrelated `thread-name` logic or storage
changes
- ensure the refresh paths keep the cached footer in sync when the
service tier (fast mode) changes
Testing
- Manually tested
Here's what it looks like when enabled:
<img width="366" height="75" alt="image"
src="https://github.com/user-attachments/assets/7f992d2b-6dab-49ed-aa43-ad496f56f193"
/>
#### What
Add structured `@plugin` parsing and TUI support for plugin mentions.
- Core: switch from plain-text `@display_name` parsing to structured
`plugin://...` mentions via `UserInput::Mention` and
`[$...](plugin://...)` links in text, same pattern as apps/skills.
- TUI: add plugin mention popup, autocomplete, and chips when typing
`$`. Load plugin capability summaries and feed them into the composer;
plugin mentions appear alongside skills and apps.
- Generalize mention parsing to a sigil parameter, still defaults to `$`
<img width="797" height="119" alt="image"
src="https://github.com/user-attachments/assets/f0fe2658-d908-4927-9139-73f850805ceb"
/>
Builds on #13510. Currently clients have to build their own `id` via
`plugin@marketplace` and filter plugins to show by `enabled`, but we
will add `id` and `available` as fields returned from `plugin/list`
soon.
####Tests
Added tests, verified locally.
## Summary
- render pending steer previews with a single `pending steer:` prefix
instead of repeating it for each source line
- reuse the same truncation path for pending steers and queued drafts so
multiline previews behave consistently
- add snapshot coverage for the multiline pending steer case
Before
<img width="969" height="219" alt="Screenshot 2026-03-05 at 3 55 11 PM"
src="https://github.com/user-attachments/assets/b062c9c8-43d3-4a52-98e0-3c7643d1697b"
/>
After
<img width="965" height="203" alt="Screenshot 2026-03-05 at 3 56 08 PM"
src="https://github.com/user-attachments/assets/40935863-55b3-444f-9e14-1ac63126b2e1"
/>
## Codex author
`codex resume 019cc054-385e-79a3-bb85-ec9499623bd8`
Co-authored-by: Codex <noreply@openai.com>
## Note-- added plugin mentions via @, but that conflicts with file
mentions
depends and builds upon #13433.
- introduces explicit `@plugin` mentions. this injects the plugin's mcp
servers, app names, and skill name format into turn context as a dev
message.
- we do not yet have UI for these mentions, so we currently parse raw
text (as opposed to skills and apps which have UI chips, autocomplete,
etc.) this depends on a `plugins/list` app-server endpoint we can feed
the UI with, which is upcoming
- also annotate mcp and app tool descriptions with the plugin(s) they
come from. this gives the model a first class way of understanding what
tools come from which plugins, which will help implicit invocation.
### Tests
Added and updated tests, unit and integration. Also confirmed locally a
raw `@plugin` injects the dev message, and the model knows about its
apps, mcps, and skills.
This adds a first-class server request for MCP server elicitations:
`mcpServer/elicitation/request`.
Until now, MCP elicitation requests only showed up as a raw
`codex/event/elicitation_request` event from core. That made it hard for
v2 clients to handle elicitations using the same request/response flow
as other server-driven interactions (like shell and `apply_patch`
tools).
This also updates the underlying MCP elicitation request handling in
core to pass through the full MCP request (including URL and form data)
so we can expose it properly in app-server.
### Why not `item/mcpToolCall/elicitationRequest`?
This is because MCP elicitations are related to MCP servers first, and
only optionally to a specific MCP tool call.
In the MCP protocol, elicitation is a server-to-client capability: the
server sends `elicitation/create`, and the client replies with an
elicitation result. RMCP models it that way as well.
In practice an elicitation is often triggered by an MCP tool call, but
not always.
### What changed
- add `mcpServer/elicitation/request` to the v2 app-server API
- translate core `codex/event/elicitation_request` events into the new
v2 server request
- map client responses back into `Op::ResolveElicitation` so the MCP
server can continue
- update app-server docs and generated protocol schema
- add an end-to-end app-server test that covers the full round trip
through a real RMCP elicitation flow
- The new test exercises a realistic case where an MCP tool call
triggers an elicitation, the app-server emits
mcpServer/elicitation/request, the client accepts it, and the tool call
resumes and completes successfully.
### app-server API flow
- Client starts a thread with `thread/start`.
- Client starts a turn with `turn/start`.
- App-server sends `item/started` for the `mcpToolCall`.
- While that tool call is in progress, app-server sends
`mcpServer/elicitation/request`.
- Client responds to that request with `{ action: "accept" | "decline" |
"cancel" }`.
- App-server sends `serverRequest/resolved`.
- App-server sends `item/completed` for the mcpToolCall.
- App-server sends `turn/completed`.
- If the turn is interrupted while the elicitation is pending,
app-server still sends `serverRequest/resolved` before the turn
finishes.
## Summary
Changes the permission profile shape from a bare network boolean to a
nested object.
Before:
```yaml
permissions:
network: true
```
After:
```yaml
permissions:
network:
enabled: true
```
This also updates the shared Rust and app-server protocol types so
`PermissionProfile.network` is no longer `Option<bool>`, but
`Option<NetworkPermissions>` with `enabled: Option<bool>`.
## What Changed
- Updated `PermissionProfile` in `codex-rs/protocol/src/models.rs`:
- `pub network: Option<bool>` -> `pub network:
Option<NetworkPermissions>`
- Added `NetworkPermissions` with:
- `pub enabled: Option<bool>`
- Changed emptiness semantics so `network` is only considered empty when
`enabled` is `None`
- Updated skill metadata parsing to accept `permissions.network.enabled`
- Updated core permission consumers to read
`network.enabled.unwrap_or(false)` where a concrete boolean is needed
- Updated app-server v2 protocol types and regenerated schema/TypeScript
outputs
- Updated docs to mention `additionalPermissions.network.enabled`
## Summary
`PermissionProfile.network` could not be preserved when additional or
compiled permissions resolved to
`SandboxPolicy::ReadOnly`, because `ReadOnly` had no network_access
field. This change makes read-only + network
enabled representable directly and threads that through the protocol,
app-server v2 mirror, and permission-
merging logic.
## What changed
- Added `network_access: bool` to `SandboxPolicy::ReadOnly` in the core
protocol and app-server v2 protocol.
- Kept backward compatibility by defaulting the new field to false, so
legacy read-only payloads still
deserialize unchanged.
- Updated `has_full_network_access()` and sandbox summaries to respect
read-only network access.
- Preserved PermissionProfile.network when:
- compiling skill permission profiles into sandbox policies
- normalizing additional permissions
- merging additional permissions into existing sandbox policies
- Updated the approval overlay to show network in the rendered
permission rule when requested.
- Regenerated app-server schema fixtures for the new v2 wire shape.
## Summary
- submit `Enter` steers immediately while a turn is already running
instead of routing them through `queued_user_messages`
- keep those submitted steers visible in the footer as `pending_steers`
until core records them as a user message or aborts the turn
- reconcile pending steers on `ItemCompleted(UserMessage)`, not
`RawResponseItem`
- emit user-message item lifecycle for leftover pending input at task
finish, then remove the TUI `TurnComplete` fallback
- keep `queued_user_messages` for actual queued drafts, rendered below
pending steers
## Problem
While the assistant was generating, pressing `Enter` could send the
input into `queued_user_messages`. That queue only drains after the turn
ends, so ordinary steers behaved like queued drafts instead of landing
at the next core sampling boundary.
The first version of this fix also used `RawResponseItem` to decide when
a steer had landed. Review feedback was that this is the wrong
abstraction for client behavior.
There was also a late edge case in core: if pending steer input was
accepted after the final sampling decision but before `TurnComplete`,
core would record that user message into history at task finish without
emitting `ItemStarted(UserMessage)` / `ItemCompleted(UserMessage)`. TUI
had a fallback to paper over that gap locally.
## Approach
- `Enter` during an active turn now submits a normal `Op::UserTurn`
immediately
- TUI keeps a local pending-steer preview instead of rendering that user
message into history immediately
- when core records the steer as `ItemCompleted(UserMessage)`, TUI
matches and removes the corresponding pending preview, then renders the
committed user message
- core now emits the same user-message lifecycle when
`on_task_finished(...)` drains leftover pending user input, before
`TurnComplete`
- with that lifecycle gap closed in core, TUI no longer needs to flush
pending steers into history on `TurnComplete`
- if the turn is interrupted, pending steers and queued drafts are both
restored into the composer, with pending steers first
## Notes
- `Tab` still uses the real queued-message path
- `queued_user_messages` and `pending_steers` are separate state with
separate semantics
- the pending-steer matching key is built directly from `UserInput`
- this removes the new TUI dependency on `RawResponseItem`
## Validation
- `just fmt`
- `cargo test -p codex-core
task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input --
--nocapture`
- `cargo test -p codex-tui`