Files
codex/codex-rs/codex-mcp/src/client_capabilities.rs
T
riley-oai f326857cf4 Restrict MCP user verification and add workspace-scoped identity (#43524)
## Why

Configured MCP servers must not be able to request user verification, even when they reuse the hosted apps server name. Local verification identity selection also needs to distinguish a user's workspace memberships.

## What changed

- Advertise `userVerification` only to the host-owned apps MCP server and cancel verification requests from other servers before prompting the client.
- Add `CodexAuth::get_chatgpt_account_user_id()` to read the access token's opaque `chatgpt_account_user_id` only when its workspace matches the selected account. Missing or malformed claims return no identity, without falling back to another user ID or breaking ordinary authentication.

## Testing

Add coverage for capability filtering, rejection of configured servers including those using the hosted apps name, and account-user identity selection with malformed or mismatched claims. Add an MCP round-trip test showing that verification waits for a client response even in full access mode, returns the supplied proof, and discards response metadata.

GitOrigin-RevId: 156e83d5a0302c013b85e564b46b4bfe858cbdb0
2026-09-07 17:26:10 +00:00

74 lines
2.5 KiB
Rust

use std::collections::HashMap;
use codex_protocol::mcp::ClientMcpExtensions;
use codex_protocol::mcp::MCP_APP_UI_EXTENSION_ID;
use codex_protocol::mcp::OPENAI_ELICITATION_EXTENSION_ID;
use codex_protocol::mcp::OPENAI_FORM_EXTENSION_ID;
use codex_protocol::mcp::OPENAI_STANDARD_FORM_INPUT_EXTENSION_ID;
use serde_json::Map;
use serde_json::Value;
/// Restricts verification to the host-owned plugin service, even when another server uses its name.
pub(crate) fn server_mcp_extensions(
extensions: &ClientMcpExtensions,
server_name: &str,
server: Option<&crate::catalog::ResolvedMcpServer>,
) -> ClientMcpExtensions {
let plugin_service = server.is_some_and(|server| {
server
.source()
.is_host_owned_apps(server_name, server.config())
});
ClientMcpExtensions::new(extensions.iter().map(|(id, settings)| {
let mut settings = settings.clone();
if !plugin_service
&& id == OPENAI_ELICITATION_EXTENSION_ID
&& let Some(settings) = settings.as_object_mut()
{
settings.remove("userVerification");
}
(id.to_string(), settings)
}))
}
/// Selects the MCP extensions Codex supports from those declared by the app-server host.
///
/// App-server clients may declare unrelated extensions. Codex retains only the
/// trusted extension namespaces it knows how to project downstream. The
/// legacy form capability is normalized into the same extension map.
pub fn client_mcp_extensions(
extensions: Option<&HashMap<String, Value>>,
legacy_openai_form_elicitation: bool,
) -> ClientMcpExtensions {
let mut selected = extensions
.into_iter()
.flat_map(HashMap::iter)
.filter(|(id, _)| {
matches!(
id.as_str(),
OPENAI_FORM_EXTENSION_ID
| OPENAI_ELICITATION_EXTENSION_ID
| OPENAI_STANDARD_FORM_INPUT_EXTENSION_ID
| MCP_APP_UI_EXTENSION_ID
)
})
.map(|(id, value)| (id.clone(), value.clone()))
.collect::<HashMap<_, _>>();
if let Some(settings) = selected
.get_mut(OPENAI_ELICITATION_EXTENSION_ID)
.and_then(Value::as_object_mut)
{
settings.retain(|key, _| key == "form");
}
if legacy_openai_form_elicitation {
selected
.entry(OPENAI_FORM_EXTENSION_ID.to_string())
.or_insert_with(|| Value::Object(Map::new()));
}
ClientMcpExtensions::new(selected)
}
#[cfg(test)]
#[path = "client_capabilities_tests.rs"]
mod tests;