mirror of
https://github.com/openai/codex.git
synced 2026-11-14 21:40:39 +00:00
Advertise MCP App UI support to Codex Apps
This commit is contained in:
@@ -793,12 +793,7 @@ fn sample_initialize_fact(connection_id: u64) -> AnalyticsFact {
|
||||
title: None,
|
||||
version: "1.0.0".to_string(),
|
||||
},
|
||||
capabilities: Some(InitializeCapabilities {
|
||||
experimental_api: false,
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: None,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
}),
|
||||
capabilities: Some(InitializeCapabilities::default()),
|
||||
},
|
||||
product_client_id: DEFAULT_ORIGINATOR.to_string(),
|
||||
runtime: CodexRuntimeMetadata {
|
||||
@@ -1674,12 +1669,7 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize
|
||||
title: None,
|
||||
version: "1.0.0".to_string(),
|
||||
},
|
||||
capabilities: Some(InitializeCapabilities {
|
||||
experimental_api: false,
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: None,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
}),
|
||||
capabilities: Some(InitializeCapabilities::default()),
|
||||
},
|
||||
product_client_id: DEFAULT_ORIGINATOR.to_string(),
|
||||
runtime: CodexRuntimeMetadata {
|
||||
@@ -1978,12 +1968,7 @@ async fn compaction_event_ingests_custom_fact() {
|
||||
title: None,
|
||||
version: "1.0.0".to_string(),
|
||||
},
|
||||
capabilities: Some(InitializeCapabilities {
|
||||
experimental_api: false,
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: None,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
}),
|
||||
capabilities: Some(InitializeCapabilities::default()),
|
||||
},
|
||||
product_client_id: DEFAULT_ORIGINATOR.to_string(),
|
||||
runtime: sample_runtime_metadata(),
|
||||
@@ -2108,12 +2093,7 @@ async fn guardian_review_event_ingests_custom_fact_with_optional_target_item() {
|
||||
title: None,
|
||||
version: "1.0.0".to_string(),
|
||||
},
|
||||
capabilities: Some(InitializeCapabilities {
|
||||
experimental_api: false,
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: None,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
}),
|
||||
capabilities: Some(InitializeCapabilities::default()),
|
||||
},
|
||||
product_client_id: DEFAULT_ORIGINATOR.to_string(),
|
||||
runtime: sample_runtime_metadata(),
|
||||
|
||||
@@ -374,6 +374,7 @@ impl InProcessClientStartArgs {
|
||||
Some(self.opt_out_notification_methods.clone())
|
||||
},
|
||||
mcp_server_openai_form_elicitation: self.mcp_server_openai_form_elicitation,
|
||||
extensions: None,
|
||||
};
|
||||
|
||||
InitializeParams {
|
||||
|
||||
@@ -101,6 +101,7 @@ impl RemoteAppServerConnectArgs {
|
||||
Some(self.opt_out_notification_methods.clone())
|
||||
},
|
||||
mcp_server_openai_form_elicitation: self.mcp_server_openai_form_elicitation,
|
||||
extensions: None,
|
||||
};
|
||||
|
||||
InitializeParams {
|
||||
|
||||
@@ -1279,6 +1279,14 @@
|
||||
"description": "Opt into receiving experimental API methods and fields.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"extensions": {
|
||||
"additionalProperties": true,
|
||||
"description": "MCP extension settings declared by the app-server client.\n\nCodex currently consumes `io.modelcontextprotocol/ui` so widget-capable hosts can advertise the same UI support to downstream MCP servers.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"mcpServerOpenaiFormElicitation": {
|
||||
"description": "Allow downstream MCP servers to request OpenAI extended form elicitations.",
|
||||
"type": "boolean"
|
||||
|
||||
+8
@@ -2923,6 +2923,14 @@
|
||||
"description": "Opt into receiving experimental API methods and fields.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"extensions": {
|
||||
"additionalProperties": true,
|
||||
"description": "MCP extension settings declared by the app-server client.\n\nCodex currently consumes `io.modelcontextprotocol/ui` so widget-capable hosts can advertise the same UI support to downstream MCP servers.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"mcpServerOpenaiFormElicitation": {
|
||||
"description": "Allow downstream MCP servers to request OpenAI extended form elicitations.",
|
||||
"type": "boolean"
|
||||
|
||||
+8
@@ -7524,6 +7524,14 @@
|
||||
"description": "Opt into receiving experimental API methods and fields.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"extensions": {
|
||||
"additionalProperties": true,
|
||||
"description": "MCP extension settings declared by the app-server client.\n\nCodex currently consumes `io.modelcontextprotocol/ui` so widget-capable hosts can advertise the same UI support to downstream MCP servers.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"mcpServerOpenaiFormElicitation": {
|
||||
"description": "Allow downstream MCP servers to request OpenAI extended form elicitations.",
|
||||
"type": "boolean"
|
||||
|
||||
@@ -30,6 +30,14 @@
|
||||
"description": "Opt into receiving experimental API methods and fields.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"extensions": {
|
||||
"additionalProperties": true,
|
||||
"description": "MCP extension settings declared by the app-server client.\n\nCodex currently consumes `io.modelcontextprotocol/ui` so widget-capable hosts can advertise the same UI support to downstream MCP servers.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"mcpServerOpenaiFormElicitation": {
|
||||
"description": "Allow downstream MCP servers to request OpenAI extended form elicitations.",
|
||||
"type": "boolean"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { JsonValue } from "./serde_json/JsonValue";
|
||||
|
||||
/**
|
||||
* Client-declared capabilities negotiated during initialize.
|
||||
@@ -22,4 +23,11 @@ mcpServerOpenaiFormElicitation?: boolean,
|
||||
* Exact notification method names that should be suppressed for this
|
||||
* connection (for example `thread/started`).
|
||||
*/
|
||||
optOutNotificationMethods?: Array<string> | null, };
|
||||
optOutNotificationMethods?: Array<string> | null,
|
||||
/**
|
||||
* MCP extension settings declared by the app-server client.
|
||||
*
|
||||
* Codex currently consumes `io.modelcontextprotocol/ui` so widget-capable
|
||||
* hosts can advertise the same UI support to downstream MCP servers.
|
||||
*/
|
||||
extensions?: { [key in string]?: JsonValue } | null, };
|
||||
|
||||
@@ -2207,6 +2207,12 @@ mod tests {
|
||||
"thread/started".to_string(),
|
||||
"item/agentMessage/delta".to_string(),
|
||||
]),
|
||||
extensions: Some(std::collections::HashMap::from([(
|
||||
"io.modelcontextprotocol/ui".to_string(),
|
||||
json!({
|
||||
"mimeTypes": ["text/html;profile=mcp-app"],
|
||||
}),
|
||||
)])),
|
||||
}),
|
||||
},
|
||||
};
|
||||
@@ -2228,7 +2234,12 @@ mod tests {
|
||||
"optOutNotificationMethods": [
|
||||
"thread/started",
|
||||
"item/agentMessage/delta"
|
||||
]
|
||||
],
|
||||
"extensions": {
|
||||
"io.modelcontextprotocol/ui": {
|
||||
"mimeTypes": ["text/html;profile=mcp-app"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -2255,7 +2266,12 @@ mod tests {
|
||||
"optOutNotificationMethods": [
|
||||
"thread/started",
|
||||
"item/agentMessage/delta"
|
||||
]
|
||||
],
|
||||
"extensions": {
|
||||
"io.modelcontextprotocol/ui": {
|
||||
"mimeTypes": ["text/html;profile=mcp-app"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))?;
|
||||
@@ -2278,6 +2294,12 @@ mod tests {
|
||||
"thread/started".to_string(),
|
||||
"item/agentMessage/delta".to_string(),
|
||||
]),
|
||||
extensions: Some(std::collections::HashMap::from([(
|
||||
"io.modelcontextprotocol/ui".to_string(),
|
||||
json!({
|
||||
"mimeTypes": ["text/html;profile=mcp-app"],
|
||||
}),
|
||||
)])),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -57,6 +57,13 @@ pub struct InitializeCapabilities {
|
||||
/// connection (for example `thread/started`).
|
||||
#[ts(optional = nullable)]
|
||||
pub opt_out_notification_methods: Option<Vec<String>>,
|
||||
/// MCP extension settings declared by the app-server client.
|
||||
///
|
||||
/// Codex currently consumes `io.modelcontextprotocol/ui` so widget-capable
|
||||
/// hosts can advertise the same UI support to downstream MCP servers.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional = nullable)]
|
||||
pub extensions: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
|
||||
@@ -1671,6 +1671,7 @@ impl CodexClient {
|
||||
.collect(),
|
||||
),
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
extensions: None,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -225,6 +225,7 @@ pub(crate) struct InitializedConnectionSessionState {
|
||||
pub(crate) client_version: String,
|
||||
pub(crate) request_attestation: bool,
|
||||
pub(crate) supports_openai_form_elicitation: bool,
|
||||
pub(crate) supports_mcp_app_ui_webview: bool,
|
||||
}
|
||||
|
||||
impl Default for ConnectionSessionState {
|
||||
@@ -281,6 +282,12 @@ impl ConnectionSessionState {
|
||||
.get()
|
||||
.is_some_and(|session| session.supports_openai_form_elicitation)
|
||||
}
|
||||
|
||||
pub(crate) fn supports_mcp_app_ui_webview(&self) -> bool {
|
||||
self.initialized
|
||||
.get()
|
||||
.is_some_and(|session| session.supports_mcp_app_ui_webview)
|
||||
}
|
||||
pub(crate) fn initialize(&self, session: InitializedConnectionSessionState) -> Result<(), ()> {
|
||||
self.initialized.set(session).map_err(|_| ())
|
||||
}
|
||||
@@ -897,6 +904,7 @@ impl MessageProcessor {
|
||||
let app_server_client_name = session.app_server_client_name().map(str::to_string);
|
||||
let client_version = session.client_version().map(str::to_string);
|
||||
let supports_openai_form_elicitation = session.supports_openai_form_elicitation();
|
||||
let supports_mcp_app_ui_webview = session.supports_mcp_app_ui_webview();
|
||||
let error_request_id = connection_request_id.clone();
|
||||
let rpc_gate = Arc::clone(&session.rpc_gate);
|
||||
let processor = Arc::clone(self);
|
||||
@@ -913,6 +921,7 @@ impl MessageProcessor {
|
||||
app_server_client_name,
|
||||
client_version,
|
||||
supports_openai_form_elicitation,
|
||||
supports_mcp_app_ui_webview,
|
||||
)
|
||||
.await;
|
||||
if let Err(error) = result {
|
||||
@@ -935,6 +944,7 @@ impl MessageProcessor {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn handle_initialized_client_request(
|
||||
self: Arc<Self>,
|
||||
connection_request_id: ConnectionRequestId,
|
||||
@@ -943,6 +953,7 @@ impl MessageProcessor {
|
||||
app_server_client_name: Option<String>,
|
||||
client_version: Option<String>,
|
||||
supports_openai_form_elicitation: bool,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let connection_id = connection_request_id.connection_id;
|
||||
let request_id = ConnectionRequestId {
|
||||
@@ -1099,6 +1110,7 @@ impl MessageProcessor {
|
||||
app_server_client_name.clone(),
|
||||
client_version.clone(),
|
||||
supports_openai_form_elicitation,
|
||||
supports_mcp_app_ui_webview,
|
||||
request_context,
|
||||
)
|
||||
.await
|
||||
@@ -1117,6 +1129,7 @@ impl MessageProcessor {
|
||||
client_version.clone(),
|
||||
/*supports_openai_form_elicitation*/
|
||||
supports_openai_form_elicitation,
|
||||
supports_mcp_app_ui_webview,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1129,6 +1142,7 @@ impl MessageProcessor {
|
||||
client_version.clone(),
|
||||
/*supports_openai_form_elicitation*/
|
||||
supports_openai_form_elicitation,
|
||||
supports_mcp_app_ui_webview,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1330,6 +1344,7 @@ impl MessageProcessor {
|
||||
client_version.clone(),
|
||||
/*supports_openai_form_elicitation*/
|
||||
supports_openai_form_elicitation,
|
||||
supports_mcp_app_ui_webview,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1346,7 +1361,7 @@ impl MessageProcessor {
|
||||
}
|
||||
ClientRequest::ThreadRealtimeStart { params, .. } => {
|
||||
self.turn_processor
|
||||
.thread_realtime_start(&request_id, params)
|
||||
.thread_realtime_start(&request_id, params, supports_mcp_app_ui_webview)
|
||||
.await
|
||||
}
|
||||
ClientRequest::ThreadRealtimeAppendAudio { params, .. } => {
|
||||
@@ -1373,7 +1388,9 @@ impl MessageProcessor {
|
||||
self.turn_processor.thread_realtime_list_voices().await
|
||||
}
|
||||
ClientRequest::ReviewStart { params, .. } => {
|
||||
self.turn_processor.review_start(&request_id, params).await
|
||||
self.turn_processor
|
||||
.review_start(&request_id, params, supports_mcp_app_ui_webview)
|
||||
.await
|
||||
}
|
||||
ClientRequest::McpServerOauthLogin { params, .. } => {
|
||||
self.mcp_processor.mcp_server_oauth_login(params).await
|
||||
@@ -1393,7 +1410,7 @@ impl MessageProcessor {
|
||||
}
|
||||
ClientRequest::McpServerToolCall { params, .. } => {
|
||||
self.mcp_processor
|
||||
.mcp_server_tool_call(&request_id, params)
|
||||
.mcp_server_tool_call(&request_id, params, supports_mcp_app_ui_webview)
|
||||
.await
|
||||
}
|
||||
ClientRequest::WindowsSandboxSetupStart { params, .. } => {
|
||||
|
||||
@@ -71,6 +71,8 @@ impl InitializeRequestProcessor {
|
||||
let experimental_api_enabled = capabilities.experimental_api;
|
||||
let request_attestation = capabilities.request_attestation;
|
||||
let supports_openai_form_elicitation = capabilities.mcp_server_openai_form_elicitation;
|
||||
let supports_mcp_app_ui_webview =
|
||||
codex_mcp::supports_mcp_app_ui_webview(capabilities.extensions.as_ref());
|
||||
let opt_out_notification_methods = capabilities
|
||||
.opt_out_notification_methods
|
||||
.unwrap_or_default();
|
||||
@@ -98,6 +100,7 @@ impl InitializeRequestProcessor {
|
||||
client_version: version,
|
||||
request_attestation,
|
||||
supports_openai_form_elicitation,
|
||||
supports_mcp_app_ui_webview,
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
|
||||
@@ -67,8 +67,9 @@ impl McpRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: McpServerToolCallParams,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.call_mcp_server_tool(request_id, params)
|
||||
self.call_mcp_server_tool(request_id, params, supports_mcp_app_ui_webview)
|
||||
.await
|
||||
.map(|()| None)
|
||||
}
|
||||
@@ -456,16 +457,22 @@ impl McpRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: McpServerToolCallParams,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let outgoing = Arc::clone(&self.outgoing);
|
||||
let thread_id = params.thread_id.clone();
|
||||
let (_, thread) = self.load_thread(&thread_id).await?;
|
||||
let mcp_runtime = thread
|
||||
.current_mcp_runtime_with_mcp_app_ui_webview_support(supports_mcp_app_ui_webview)
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to resolve MCP runtime: {err}")))?;
|
||||
let meta = with_mcp_tool_call_thread_id_meta(params.meta, &thread_id);
|
||||
let request_id = request_id.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let result = thread
|
||||
.call_mcp_tool(¶ms.server, ¶ms.tool, params.arguments, meta)
|
||||
let result = mcp_runtime
|
||||
.manager()
|
||||
.call_tool(¶ms.server, ¶ms.tool, params.arguments, meta)
|
||||
.await
|
||||
.map(McpServerToolCallResponse::from)
|
||||
.map_err(|error| internal_error(format!("{error:#}")));
|
||||
|
||||
@@ -419,6 +419,7 @@ impl ThreadRequestProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn thread_start(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
@@ -426,6 +427,7 @@ impl ThreadRequestProcessor {
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
supports_openai_form_elicitation: bool,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
request_context: RequestContext,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.thread_start_inner(
|
||||
@@ -434,6 +436,7 @@ impl ThreadRequestProcessor {
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
supports_openai_form_elicitation,
|
||||
supports_mcp_app_ui_webview,
|
||||
request_context,
|
||||
)
|
||||
.await
|
||||
@@ -457,6 +460,7 @@ impl ThreadRequestProcessor {
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
supports_openai_form_elicitation: bool,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.thread_resume_inner(
|
||||
request_id,
|
||||
@@ -464,6 +468,7 @@ impl ThreadRequestProcessor {
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
supports_openai_form_elicitation,
|
||||
supports_mcp_app_ui_webview,
|
||||
)
|
||||
.await
|
||||
.map(|()| None)
|
||||
@@ -476,6 +481,7 @@ impl ThreadRequestProcessor {
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
supports_openai_form_elicitation: bool,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.thread_fork_inner(
|
||||
request_id,
|
||||
@@ -483,6 +489,7 @@ impl ThreadRequestProcessor {
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
supports_openai_form_elicitation,
|
||||
supports_mcp_app_ui_webview,
|
||||
)
|
||||
.await
|
||||
.map(|()| None)
|
||||
@@ -776,6 +783,7 @@ impl ThreadRequestProcessor {
|
||||
thread: &CodexThread,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let mcp_elicitations_auto_deny = xcode_26_4_mcp_elicitations_auto_deny(
|
||||
app_server_client_name.as_deref(),
|
||||
@@ -785,6 +793,7 @@ impl ThreadRequestProcessor {
|
||||
.set_app_server_client_info(
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
Some(supports_mcp_app_ui_webview),
|
||||
mcp_elicitations_auto_deny,
|
||||
)
|
||||
.await
|
||||
@@ -899,6 +908,7 @@ impl ThreadRequestProcessor {
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn thread_start_inner(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
@@ -906,6 +916,7 @@ impl ThreadRequestProcessor {
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
supports_openai_form_elicitation: bool,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
request_context: RequestContext,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let ThreadStartParams {
|
||||
@@ -982,6 +993,7 @@ impl ThreadRequestProcessor {
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
supports_openai_form_elicitation,
|
||||
supports_mcp_app_ui_webview,
|
||||
config,
|
||||
typesafe_overrides,
|
||||
dynamic_tools,
|
||||
@@ -1059,6 +1071,7 @@ impl ThreadRequestProcessor {
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
supports_openai_form_elicitation: bool,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
config_overrides: Option<HashMap<String, serde_json::Value>>,
|
||||
typesafe_overrides: ConfigOverrides,
|
||||
dynamic_tools: Option<Vec<DynamicToolSpec>>,
|
||||
@@ -1145,6 +1158,8 @@ impl ThreadRequestProcessor {
|
||||
.map_err(|err| config_load_error(&err))?;
|
||||
}
|
||||
|
||||
config.supports_mcp_app_ui_webview = supports_mcp_app_ui_webview;
|
||||
|
||||
if let Ok(Some(err)) =
|
||||
codex_core::check_execpolicy_for_warnings(&config.config_layer_stack).await
|
||||
{
|
||||
@@ -1230,6 +1245,7 @@ impl ThreadRequestProcessor {
|
||||
thread.as_ref(),
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
supports_mcp_app_ui_webview,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -2640,6 +2656,7 @@ impl ThreadRequestProcessor {
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
supports_openai_form_elicitation: bool,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
if let Ok(thread_id) = ThreadId::from_string(¶ms.thread_id)
|
||||
&& self
|
||||
@@ -2684,6 +2701,7 @@ impl ThreadRequestProcessor {
|
||||
¶ms,
|
||||
app_server_client_name.clone(),
|
||||
app_server_client_version.clone(),
|
||||
supports_mcp_app_ui_webview,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -2762,7 +2780,7 @@ impl ThreadRequestProcessor {
|
||||
.await;
|
||||
|
||||
// Derive a Config using the same logic as new conversation, honoring overrides if provided.
|
||||
let config = match self
|
||||
let mut config = match self
|
||||
.config_manager
|
||||
.load_for_cwd(request_overrides, typesafe_overrides, history_cwd)
|
||||
.await
|
||||
@@ -2774,6 +2792,7 @@ impl ThreadRequestProcessor {
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
config.supports_mcp_app_ui_webview = supports_mcp_app_ui_webview;
|
||||
|
||||
let response_history = thread_history.clone();
|
||||
|
||||
@@ -2798,6 +2817,7 @@ impl ThreadRequestProcessor {
|
||||
codex_thread.as_ref(),
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
supports_mcp_app_ui_webview,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -2977,6 +2997,7 @@ impl ThreadRequestProcessor {
|
||||
params: &ThreadResumeParams,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
) -> Result<RunningThreadResumeResult, JSONRPCErrorError> {
|
||||
let running_thread = if params.history.is_some() {
|
||||
if let Ok(existing_thread_id) = ThreadId::from_string(¶ms.thread_id)
|
||||
@@ -3105,6 +3126,7 @@ impl ThreadRequestProcessor {
|
||||
existing_thread.as_ref(),
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
supports_mcp_app_ui_webview,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -3405,6 +3427,7 @@ impl ThreadRequestProcessor {
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
supports_openai_form_elicitation: bool,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let ThreadForkParams {
|
||||
thread_id,
|
||||
@@ -3498,11 +3521,12 @@ impl ThreadRequestProcessor {
|
||||
);
|
||||
typesafe_overrides.ephemeral = ephemeral.then_some(true);
|
||||
// Derive a Config using the same logic as new conversation, honoring overrides if provided.
|
||||
let config = self
|
||||
let mut config = self
|
||||
.config_manager
|
||||
.load_for_cwd(request_overrides, typesafe_overrides, history_cwd)
|
||||
.await
|
||||
.map_err(|err| config_load_error(&err))?;
|
||||
config.supports_mcp_app_ui_webview = supports_mcp_app_ui_webview;
|
||||
|
||||
let fallback_model_provider = config.model_provider_id.clone();
|
||||
|
||||
@@ -3538,6 +3562,7 @@ impl ThreadRequestProcessor {
|
||||
forked_thread.as_ref(),
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
supports_mcp_app_ui_webview,
|
||||
)
|
||||
.await?;
|
||||
if session_configured.rollout_path.is_some()
|
||||
|
||||
@@ -159,6 +159,7 @@ impl TurnRequestProcessor {
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
supports_openai_form_elicitation: bool,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
validate_user_input_image_urls(¶ms.input)?;
|
||||
self.turn_start_inner(
|
||||
@@ -167,6 +168,7 @@ impl TurnRequestProcessor {
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
/*supports_openai_form_elicitation*/ supports_openai_form_elicitation,
|
||||
supports_mcp_app_ui_webview,
|
||||
)
|
||||
.await
|
||||
.map(|response| Some(response.into()))
|
||||
@@ -216,8 +218,9 @@ impl TurnRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ThreadRealtimeStartParams,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.thread_realtime_start_inner(request_id, params)
|
||||
self.thread_realtime_start_inner(request_id, params, supports_mcp_app_ui_webview)
|
||||
.await
|
||||
.map(|response| response.map(Into::into))
|
||||
}
|
||||
@@ -277,8 +280,9 @@ impl TurnRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ReviewStartParams,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.review_start_inner(request_id, params)
|
||||
self.review_start_inner(request_id, params, supports_mcp_app_ui_webview)
|
||||
.await
|
||||
.map(|()| None)
|
||||
}
|
||||
@@ -419,6 +423,22 @@ impl TurnRequestProcessor {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn submit_core_op_with_mcp_app_ui_webview_support(
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
thread: &CodexThread,
|
||||
op: Op,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
) -> CodexResult<String> {
|
||||
thread
|
||||
.submit_with_trace_and_mcp_app_ui_webview_support(
|
||||
op,
|
||||
self.request_trace_context(request_id).await,
|
||||
supports_mcp_app_ui_webview,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn input_too_large_error(actual_chars: usize) -> JSONRPCErrorError {
|
||||
let mut error = invalid_params(format!(
|
||||
"Input exceeds the maximum length of {MAX_USER_INPUT_TEXT_CHARS} characters."
|
||||
@@ -446,6 +466,7 @@ impl TurnRequestProcessor {
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
supports_openai_form_elicitation: bool,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
) -> Result<TurnStartResponse, JSONRPCErrorError> {
|
||||
let (thread_id, thread) =
|
||||
self.load_thread(¶ms.thread_id)
|
||||
@@ -531,6 +552,7 @@ impl TurnRequestProcessor {
|
||||
turn_op,
|
||||
self.request_trace_context(&request_id).await,
|
||||
client_user_message_id,
|
||||
supports_mcp_app_ui_webview,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
@@ -840,6 +862,7 @@ impl TurnRequestProcessor {
|
||||
.set_app_server_client_info(
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
None,
|
||||
mcp_elicitations_auto_deny,
|
||||
)
|
||||
.await
|
||||
@@ -990,6 +1013,7 @@ impl TurnRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ThreadRealtimeStartParams,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
) -> Result<Option<ThreadRealtimeStartResponse>, JSONRPCErrorError> {
|
||||
let Some((_, thread)) = self
|
||||
.prepare_realtime_conversation_thread(request_id, ¶ms.thread_id)
|
||||
@@ -997,7 +1021,7 @@ impl TurnRequestProcessor {
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
self.submit_core_op(
|
||||
self.submit_core_op_with_mcp_app_ui_webview_support(
|
||||
request_id,
|
||||
thread.as_ref(),
|
||||
Op::RealtimeConversationStart(ConversationStartParams {
|
||||
@@ -1024,6 +1048,7 @@ impl TurnRequestProcessor {
|
||||
version: params.version,
|
||||
voice: params.voice,
|
||||
}),
|
||||
supports_mcp_app_ui_webview,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to start realtime conversation: {err}")))?;
|
||||
@@ -1178,12 +1203,14 @@ impl TurnRequestProcessor {
|
||||
review_request: ReviewRequest,
|
||||
display_text: &str,
|
||||
parent_thread_id: String,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
) -> std::result::Result<(), JSONRPCErrorError> {
|
||||
let turn_id = self
|
||||
.submit_core_op(
|
||||
.submit_core_op_with_mcp_app_ui_webview_support(
|
||||
request_id,
|
||||
parent_thread.as_ref(),
|
||||
Op::Review { review_request },
|
||||
supports_mcp_app_ui_webview,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to start review: {err}")))?;
|
||||
@@ -1200,6 +1227,7 @@ impl TurnRequestProcessor {
|
||||
parent_thread: Arc<CodexThread>,
|
||||
review_request: ReviewRequest,
|
||||
display_text: &str,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
) -> std::result::Result<(), JSONRPCErrorError> {
|
||||
parent_thread.ensure_rollout_materialized().await;
|
||||
parent_thread.flush_rollout().await.map_err(|err| {
|
||||
@@ -1217,6 +1245,7 @@ impl TurnRequestProcessor {
|
||||
})?;
|
||||
|
||||
let mut config = self.config.as_ref().clone();
|
||||
config.supports_mcp_app_ui_webview = supports_mcp_app_ui_webview;
|
||||
if let Some(review_model) = &config.review_model {
|
||||
config.model = Some(review_model.clone());
|
||||
}
|
||||
@@ -1287,10 +1316,11 @@ impl TurnRequestProcessor {
|
||||
}
|
||||
|
||||
let turn_id = self
|
||||
.submit_core_op(
|
||||
.submit_core_op_with_mcp_app_ui_webview_support(
|
||||
request_id,
|
||||
review_thread.as_ref(),
|
||||
Op::Review { review_request },
|
||||
supports_mcp_app_ui_webview,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
@@ -1309,6 +1339,7 @@ impl TurnRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ReviewStartParams,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let ReviewStartParams {
|
||||
thread_id,
|
||||
@@ -1326,6 +1357,7 @@ impl TurnRequestProcessor {
|
||||
review_request,
|
||||
&display_text,
|
||||
thread_id,
|
||||
supports_mcp_app_ui_webview,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -1336,6 +1368,7 @@ impl TurnRequestProcessor {
|
||||
parent_thread,
|
||||
review_request,
|
||||
&display_text,
|
||||
supports_mcp_app_ui_webview,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -80,8 +80,7 @@ async fn attestation_generate_round_trip_adds_header_to_responses_websocket_hand
|
||||
Some(InitializeCapabilities {
|
||||
experimental_api: true,
|
||||
request_attestation: true,
|
||||
opt_out_notification_methods: None,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
..Default::default()
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -35,12 +35,7 @@ async fn mock_experimental_method_requires_experimental_api_capability() -> Resu
|
||||
let init = mcp
|
||||
.initialize_with_capabilities(
|
||||
default_client_info(),
|
||||
Some(InitializeCapabilities {
|
||||
experimental_api: false,
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: None,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
}),
|
||||
Some(InitializeCapabilities::default()),
|
||||
)
|
||||
.await?;
|
||||
let JSONRPCMessage::Response(_) = init else {
|
||||
@@ -67,12 +62,7 @@ async fn realtime_conversation_start_requires_experimental_api_capability() -> R
|
||||
let init = mcp
|
||||
.initialize_with_capabilities(
|
||||
default_client_info(),
|
||||
Some(InitializeCapabilities {
|
||||
experimental_api: false,
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: None,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
}),
|
||||
Some(InitializeCapabilities::default()),
|
||||
)
|
||||
.await?;
|
||||
let JSONRPCMessage::Response(_) = init else {
|
||||
@@ -114,12 +104,7 @@ async fn thread_memory_mode_set_requires_experimental_api_capability() -> Result
|
||||
let init = mcp
|
||||
.initialize_with_capabilities(
|
||||
default_client_info(),
|
||||
Some(InitializeCapabilities {
|
||||
experimental_api: false,
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: None,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
}),
|
||||
Some(InitializeCapabilities::default()),
|
||||
)
|
||||
.await?;
|
||||
let JSONRPCMessage::Response(_) = init else {
|
||||
@@ -149,12 +134,7 @@ async fn thread_settings_update_requires_experimental_api_capability() -> Result
|
||||
let init = mcp
|
||||
.initialize_with_capabilities(
|
||||
default_client_info(),
|
||||
Some(InitializeCapabilities {
|
||||
experimental_api: false,
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: None,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
}),
|
||||
Some(InitializeCapabilities::default()),
|
||||
)
|
||||
.await?;
|
||||
let JSONRPCMessage::Response(_) = init else {
|
||||
@@ -184,12 +164,7 @@ async fn realtime_webrtc_start_requires_experimental_api_capability() -> Result<
|
||||
let init = mcp
|
||||
.initialize_with_capabilities(
|
||||
default_client_info(),
|
||||
Some(InitializeCapabilities {
|
||||
experimental_api: false,
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: None,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
}),
|
||||
Some(InitializeCapabilities::default()),
|
||||
)
|
||||
.await?;
|
||||
let JSONRPCMessage::Response(_) = init else {
|
||||
@@ -235,12 +210,7 @@ async fn thread_start_mock_field_requires_experimental_api_capability() -> Resul
|
||||
let init = mcp
|
||||
.initialize_with_capabilities(
|
||||
default_client_info(),
|
||||
Some(InitializeCapabilities {
|
||||
experimental_api: false,
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: None,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
}),
|
||||
Some(InitializeCapabilities::default()),
|
||||
)
|
||||
.await?;
|
||||
let JSONRPCMessage::Response(_) = init else {
|
||||
@@ -274,12 +244,7 @@ async fn thread_start_without_dynamic_tools_allows_without_experimental_api_capa
|
||||
let init = mcp
|
||||
.initialize_with_capabilities(
|
||||
default_client_info(),
|
||||
Some(InitializeCapabilities {
|
||||
experimental_api: false,
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: None,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
}),
|
||||
Some(InitializeCapabilities::default()),
|
||||
)
|
||||
.await?;
|
||||
let JSONRPCMessage::Response(_) = init else {
|
||||
@@ -312,12 +277,7 @@ async fn thread_start_granular_approval_policy_requires_experimental_api_capabil
|
||||
let init = mcp
|
||||
.initialize_with_capabilities(
|
||||
default_client_info(),
|
||||
Some(InitializeCapabilities {
|
||||
experimental_api: false,
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: None,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
}),
|
||||
Some(InitializeCapabilities::default()),
|
||||
)
|
||||
.await?;
|
||||
let JSONRPCMessage::Response(_) = init else {
|
||||
|
||||
@@ -212,9 +212,8 @@ async fn initialize_opt_out_notification_methods_filters_notifications() -> Resu
|
||||
},
|
||||
Some(InitializeCapabilities {
|
||||
experimental_api: true,
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: Some(vec!["thread/started".to_string()]),
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
..Default::default()
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -146,9 +146,8 @@ async fn thread_status_changed_can_be_opted_out() -> Result<()> {
|
||||
},
|
||||
Some(InitializeCapabilities {
|
||||
experimental_api: true,
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: Some(vec!["thread/status/changed".to_string()]),
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
..Default::default()
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rmcp::model::ExtensionCapabilities;
|
||||
use serde_json::Map;
|
||||
use serde_json::Value;
|
||||
|
||||
const MCP_APP_UI_EXTENSION_ID: &str = "io.modelcontextprotocol/ui";
|
||||
const MCP_APP_UI_WEBVIEW_MIME_TYPE: &str = "text/html;profile=mcp-app";
|
||||
const MCP_CLIENT_CAPABILITIES_META_KEY: &str = "io.modelcontextprotocol/clientCapabilities";
|
||||
|
||||
/// Returns whether the app-server host can render MCP App WebViews.
|
||||
///
|
||||
/// App-server clients may declare unrelated extensions or additional UI MIME
|
||||
/// types. Codex only forwards this one trusted capability because it is the
|
||||
/// rendering contract downstream MCP servers need for MCP App widgets.
|
||||
pub fn supports_mcp_app_ui_webview(extensions: Option<&HashMap<String, Value>>) -> bool {
|
||||
extensions
|
||||
.and_then(|extensions| extensions.get(MCP_APP_UI_EXTENSION_ID))
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|settings| settings.get("mimeTypes"))
|
||||
.and_then(Value::as_array)
|
||||
.is_some_and(|mime_types| {
|
||||
mime_types
|
||||
.iter()
|
||||
.any(|mime_type| mime_type.as_str() == Some(MCP_APP_UI_WEBVIEW_MIME_TYPE))
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn mcp_app_ui_extensions() -> ExtensionCapabilities {
|
||||
let mut settings = Map::new();
|
||||
settings.insert(
|
||||
"mimeTypes".to_string(),
|
||||
serde_json::json!([MCP_APP_UI_WEBVIEW_MIME_TYPE]),
|
||||
);
|
||||
[(MCP_APP_UI_EXTENSION_ID.to_string(), settings)]
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Adds trusted host capabilities to a Codex Apps request and removes any
|
||||
/// caller-supplied copy of the reserved metadata key.
|
||||
pub(crate) fn with_client_capabilities_meta(
|
||||
meta: Option<Value>,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
) -> Option<Value> {
|
||||
let meta = match meta {
|
||||
Some(Value::Object(mut object)) => {
|
||||
object.remove(MCP_CLIENT_CAPABILITIES_META_KEY);
|
||||
Some(Value::Object(object))
|
||||
}
|
||||
other => other,
|
||||
};
|
||||
if !supports_mcp_app_ui_webview {
|
||||
return meta;
|
||||
}
|
||||
let extensions = mcp_app_ui_extensions();
|
||||
let capabilities = serde_json::json!({ "extensions": extensions });
|
||||
let mut object = match meta {
|
||||
Some(Value::Object(object)) => object,
|
||||
_ => Map::new(),
|
||||
};
|
||||
object.insert(MCP_CLIENT_CAPABILITIES_META_KEY.to_string(), capabilities);
|
||||
Some(Value::Object(object))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn recognizes_only_the_webview_mime_type() {
|
||||
let extensions = HashMap::from([
|
||||
(
|
||||
MCP_APP_UI_EXTENSION_ID.to_string(),
|
||||
json!({
|
||||
"mimeTypes": [
|
||||
MCP_APP_UI_WEBVIEW_MIME_TYPE,
|
||||
"text/x-dil;profile=mcp-app",
|
||||
],
|
||||
}),
|
||||
),
|
||||
("example/other".to_string(), json!({"enabled": true})),
|
||||
]);
|
||||
|
||||
assert!(supports_mcp_app_ui_webview(Some(&extensions)));
|
||||
assert!(!supports_mcp_app_ui_webview(Some(&HashMap::from([(
|
||||
MCP_APP_UI_EXTENSION_ID.to_string(),
|
||||
json!({"mimeTypes": ["text/x-dil;profile=mcp-app"]}),
|
||||
),]))));
|
||||
assert!(!supports_mcp_app_ui_webview(Some(&HashMap::from([(
|
||||
MCP_APP_UI_EXTENSION_ID.to_string(),
|
||||
json!("invalid")
|
||||
),]))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trusted_capabilities_replace_spoofed_request_metadata() {
|
||||
let meta = with_client_capabilities_meta(
|
||||
Some(json!({
|
||||
MCP_CLIENT_CAPABILITIES_META_KEY: {"spoofed": true},
|
||||
"caller": "preserved",
|
||||
})),
|
||||
true,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
meta,
|
||||
Some(json!({
|
||||
"caller": "preserved",
|
||||
MCP_CLIENT_CAPABILITIES_META_KEY: {
|
||||
"extensions": {
|
||||
MCP_APP_UI_EXTENSION_ID: {
|
||||
"mimeTypes": [
|
||||
MCP_APP_UI_WEBVIEW_MIME_TYPE,
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absent_host_capabilities_remove_spoofed_request_metadata() {
|
||||
assert_eq!(
|
||||
with_client_capabilities_meta(
|
||||
Some(json!({
|
||||
MCP_CLIENT_CAPABILITIES_META_KEY: {"spoofed": true},
|
||||
"caller": "preserved",
|
||||
})),
|
||||
false,
|
||||
),
|
||||
Some(json!({"caller": "preserved"}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_caller_metadata_does_not_suppress_trusted_capabilities() {
|
||||
assert_eq!(
|
||||
with_client_capabilities_meta(Some(json!("invalid")), true),
|
||||
Some(json!({
|
||||
MCP_CLIENT_CAPABILITIES_META_KEY: {
|
||||
"extensions": {
|
||||
MCP_APP_UI_EXTENSION_ID: {
|
||||
"mimeTypes": [MCP_APP_UI_WEBVIEW_MIME_TYPE],
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::McpAuthStatusEntry;
|
||||
use crate::client_capabilities::with_client_capabilities_meta;
|
||||
use crate::codex_apps_cache::CodexAppsToolsCache;
|
||||
use crate::codex_apps_cache::CodexAppsToolsCacheKey;
|
||||
use crate::codex_apps_cache::CodexAppsToolsFetchSource;
|
||||
@@ -116,6 +117,7 @@ pub struct McpConnectionManager {
|
||||
required_servers: Vec<String>,
|
||||
tool_plugin_provenance: Arc<ToolPluginProvenance>,
|
||||
prefix_mcp_tool_names: bool,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
elicitation_requests: ElicitationRequestManager,
|
||||
startup_cancellation_token: CancellationToken,
|
||||
}
|
||||
@@ -138,6 +140,7 @@ impl McpConnectionManager {
|
||||
codex_apps_tools_cache_key: CodexAppsToolsCacheKey,
|
||||
prefix_mcp_tool_names: bool,
|
||||
client_elicitation_capability: ElicitationCapability,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
supports_openai_form_elicitation: bool,
|
||||
tool_plugin_provenance: ToolPluginProvenance,
|
||||
auth: Option<&CodexAuth>,
|
||||
@@ -222,6 +225,7 @@ impl McpConnectionManager {
|
||||
runtime_context.clone(),
|
||||
runtime_auth_provider,
|
||||
client_elicitation_capability.clone(),
|
||||
server_name == CODEX_APPS_MCP_SERVER_NAME && supports_mcp_app_ui_webview,
|
||||
supports_openai_form_elicitation,
|
||||
);
|
||||
clients.insert(server_name.clone(), async_managed_client.clone());
|
||||
@@ -273,6 +277,7 @@ impl McpConnectionManager {
|
||||
required_servers,
|
||||
tool_plugin_provenance,
|
||||
prefix_mcp_tool_names,
|
||||
supports_mcp_app_ui_webview,
|
||||
elicitation_requests: elicitation_requests.clone(),
|
||||
startup_cancellation_token: startup_cancellation_token.clone(),
|
||||
};
|
||||
@@ -358,6 +363,7 @@ impl McpConnectionManager {
|
||||
required_servers: Vec::new(),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
prefix_mcp_tool_names,
|
||||
supports_mcp_app_ui_webview: false,
|
||||
elicitation_requests: ElicitationRequestManager::new(
|
||||
approval_policy.value(),
|
||||
permission_profile.clone(),
|
||||
@@ -749,6 +755,11 @@ impl McpConnectionManager {
|
||||
));
|
||||
}
|
||||
|
||||
let meta = if server == CODEX_APPS_MCP_SERVER_NAME {
|
||||
with_client_capabilities_meta(meta, self.supports_mcp_app_ui_webview)
|
||||
} else {
|
||||
meta
|
||||
};
|
||||
let result: rmcp::model::CallToolResult = client
|
||||
.client
|
||||
.call_tool(tool.to_string(), arguments, meta, client.tool_timeout)
|
||||
|
||||
@@ -1558,6 +1558,7 @@ async fn no_local_runtime_fails_local_stdio_but_keeps_local_http_server() {
|
||||
},
|
||||
/*prefix_mcp_tool_names*/ true,
|
||||
ElicitationCapability::default(),
|
||||
/*supports_mcp_app_ui_webview*/ false,
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
ToolPluginProvenance::default(),
|
||||
/*auth*/ None,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub use client_capabilities::supports_mcp_app_ui_webview;
|
||||
pub use connection_manager::McpConnectionManager;
|
||||
pub use connection_manager::tool_is_model_visible;
|
||||
pub use elicitation::ElicitationLifecycle;
|
||||
@@ -77,6 +78,7 @@ pub use tools::declared_openai_file_input_param_names;
|
||||
|
||||
pub(crate) mod auth_elicitation;
|
||||
mod catalog;
|
||||
pub(crate) mod client_capabilities;
|
||||
pub(crate) mod codex_apps;
|
||||
pub(crate) mod codex_apps_cache;
|
||||
pub(crate) mod connection_manager;
|
||||
|
||||
@@ -145,6 +145,11 @@ pub struct McpConfig {
|
||||
pub prefix_mcp_tool_names: bool,
|
||||
/// Client-side elicitation capabilities advertised during MCP initialization.
|
||||
pub client_elicitation_capability: ElicitationCapability,
|
||||
/// Whether the active host can render MCP App WebViews.
|
||||
///
|
||||
/// This is runtime-only and is forwarded only to the host-owned Codex Apps
|
||||
/// server, never to arbitrary configured MCP servers.
|
||||
pub supports_mcp_app_ui_webview: bool,
|
||||
/// Resolved MCP registrations keyed by logical server name.
|
||||
pub mcp_server_catalog: ResolvedMcpCatalog,
|
||||
/// Plugin declarations used to attribute connector tools to plugin display names.
|
||||
@@ -335,6 +340,7 @@ pub async fn read_mcp_resource(
|
||||
codex_apps_tools_cache_key(auth),
|
||||
config.prefix_mcp_tool_names,
|
||||
config.client_elicitation_capability.clone(),
|
||||
config.supports_mcp_app_ui_webview,
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
tool_plugin_provenance(config),
|
||||
auth,
|
||||
@@ -413,6 +419,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
|
||||
codex_apps_tools_cache_key(auth),
|
||||
config.prefix_mcp_tool_names,
|
||||
config.client_elicitation_capability.clone(),
|
||||
config.supports_mcp_app_ui_webview,
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
tool_plugin_provenance,
|
||||
auth,
|
||||
|
||||
@@ -33,6 +33,7 @@ fn test_mcp_config(codex_home: PathBuf) -> McpConfig {
|
||||
apps_enabled: false,
|
||||
prefix_mcp_tool_names: true,
|
||||
client_elicitation_capability: ElicitationCapability::default(),
|
||||
supports_mcp_app_ui_webview: false,
|
||||
mcp_server_catalog: ResolvedMcpCatalog::default(),
|
||||
connector_snapshot: codex_connectors::ConnectorSnapshot::default(),
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//! [`crate::connection_manager`].
|
||||
|
||||
use std::borrow::Cow;
|
||||
#[cfg(test)]
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
@@ -275,6 +276,7 @@ struct ManagedClientStartup {
|
||||
runtime_context: McpRuntimeContext,
|
||||
runtime_auth_provider: Option<SharedAuthProvider>,
|
||||
client_elicitation_capability: ElicitationCapability,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
supports_openai_form_elicitation: bool,
|
||||
cancel_token: CancellationToken,
|
||||
startup_complete: Arc<AtomicBool>,
|
||||
@@ -293,6 +295,7 @@ impl ManagedClientStartup {
|
||||
runtime_context,
|
||||
runtime_auth_provider,
|
||||
client_elicitation_capability,
|
||||
supports_mcp_app_ui_webview,
|
||||
supports_openai_form_elicitation,
|
||||
cancel_token,
|
||||
startup_complete,
|
||||
@@ -338,6 +341,7 @@ impl ManagedClientStartup {
|
||||
elicitation_requests,
|
||||
codex_apps_tools_cache_context,
|
||||
client_elicitation_capability,
|
||||
supports_mcp_app_ui_webview,
|
||||
supports_openai_form_elicitation,
|
||||
},
|
||||
)
|
||||
@@ -391,6 +395,7 @@ impl AsyncManagedClient {
|
||||
runtime_context: McpRuntimeContext,
|
||||
runtime_auth_provider: Option<SharedAuthProvider>,
|
||||
client_elicitation_capability: ElicitationCapability,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
supports_openai_form_elicitation: bool,
|
||||
) -> Self {
|
||||
let is_codex_apps_mcp_server = server_name == CODEX_APPS_MCP_SERVER_NAME;
|
||||
@@ -419,6 +424,7 @@ impl AsyncManagedClient {
|
||||
runtime_context,
|
||||
runtime_auth_provider,
|
||||
client_elicitation_capability,
|
||||
supports_mcp_app_ui_webview,
|
||||
supports_openai_form_elicitation,
|
||||
cancel_token: cancel_token.clone(),
|
||||
startup_complete: Arc::clone(&startup_complete),
|
||||
@@ -799,10 +805,12 @@ async fn start_server_task(
|
||||
elicitation_requests,
|
||||
codex_apps_tools_cache_context,
|
||||
client_elicitation_capability,
|
||||
supports_mcp_app_ui_webview,
|
||||
supports_openai_form_elicitation,
|
||||
} = params;
|
||||
let params = mcp_initialize_request_params(
|
||||
client_elicitation_capability,
|
||||
supports_mcp_app_ui_webview,
|
||||
supports_openai_form_elicitation,
|
||||
);
|
||||
|
||||
@@ -871,16 +879,20 @@ async fn start_server_task(
|
||||
|
||||
fn mcp_initialize_request_params(
|
||||
client_elicitation_capability: ElicitationCapability,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
supports_openai_form_elicitation: bool,
|
||||
) -> InitializeRequestParams {
|
||||
let mut capabilities = ClientCapabilities::default();
|
||||
capabilities.elicitation = Some(client_elicitation_capability);
|
||||
let mut extensions = if supports_mcp_app_ui_webview {
|
||||
crate::client_capabilities::mcp_app_ui_extensions()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
if supports_openai_form_elicitation {
|
||||
capabilities.extensions = Some(BTreeMap::from([(
|
||||
OPENAI_FORM_CAPABILITY.to_string(),
|
||||
JsonObject::new(),
|
||||
)]));
|
||||
extensions.insert(OPENAI_FORM_CAPABILITY.to_string(), JsonObject::new());
|
||||
}
|
||||
capabilities.extensions = (!extensions.is_empty()).then_some(extensions);
|
||||
InitializeRequestParams::new(
|
||||
capabilities,
|
||||
Implementation::new("codex-mcp-client", env!("CARGO_PKG_VERSION")).with_title("Codex"),
|
||||
@@ -913,6 +925,7 @@ struct StartServerTaskParams {
|
||||
elicitation_requests: ElicitationRequestManager,
|
||||
codex_apps_tools_cache_context: Option<CodexAppsToolsCacheContext>,
|
||||
client_elicitation_capability: ElicitationCapability,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
supports_openai_form_elicitation: bool,
|
||||
}
|
||||
|
||||
@@ -1023,24 +1036,45 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_initialize_advertises_openai_form_only_when_supported() {
|
||||
fn mcp_initialize_advertises_extensions_only_when_supported() {
|
||||
let unsupported = mcp_initialize_request_params(
|
||||
ElicitationCapability::default(),
|
||||
/*supports_mcp_app_ui_webview*/ false,
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
);
|
||||
assert_eq!(unsupported.capabilities.extensions, None);
|
||||
|
||||
let supported = mcp_initialize_request_params(
|
||||
let form_supported = mcp_initialize_request_params(
|
||||
ElicitationCapability::default(),
|
||||
/*supports_mcp_app_ui_webview*/ false,
|
||||
/*supports_openai_form_elicitation*/ true,
|
||||
);
|
||||
assert_eq!(
|
||||
supported.capabilities.extensions,
|
||||
form_supported.capabilities.extensions,
|
||||
Some(BTreeMap::from([(
|
||||
OPENAI_FORM_CAPABILITY.to_string(),
|
||||
JsonObject::new(),
|
||||
)]))
|
||||
);
|
||||
|
||||
let ui_supported = mcp_initialize_request_params(
|
||||
ElicitationCapability::default(),
|
||||
/*supports_mcp_app_ui_webview*/ true,
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
);
|
||||
assert_eq!(
|
||||
ui_supported.capabilities.extensions,
|
||||
Some(crate::client_capabilities::mcp_app_ui_extensions())
|
||||
);
|
||||
|
||||
let both_supported = mcp_initialize_request_params(
|
||||
ElicitationCapability::default(),
|
||||
/*supports_mcp_app_ui_webview*/ true,
|
||||
/*supports_openai_form_elicitation*/ true,
|
||||
);
|
||||
let mut expected = crate::client_capabilities::mcp_app_ui_extensions();
|
||||
expected.insert(OPENAI_FORM_CAPABILITY.to_string(), JsonObject::new());
|
||||
assert_eq!(both_supported.capabilities.extensions, Some(expected));
|
||||
}
|
||||
|
||||
fn tool_with_connector_meta() -> RmcpTool {
|
||||
|
||||
@@ -250,6 +250,7 @@ pub(crate) async fn run_codex_thread_one_shot(
|
||||
id: "shutdown".to_string(),
|
||||
op: Op::Shutdown {},
|
||||
client_user_message_id: None,
|
||||
supports_mcp_app_ui_webview: None,
|
||||
trace: None,
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -156,6 +156,7 @@ async fn forward_ops_preserves_submission_trace_context() {
|
||||
id: "sub-1".to_string(),
|
||||
op: Op::Interrupt,
|
||||
client_user_message_id: None,
|
||||
supports_mcp_app_ui_webview: None,
|
||||
trace: Some(codex_protocol::protocol::W3cTraceContext {
|
||||
traceparent: Some(
|
||||
"00-1234567890abcdef1234567890abcdef-1234567890abcdef-01".to_string(),
|
||||
|
||||
@@ -258,11 +258,27 @@ impl CodexThread {
|
||||
self.codex.submit_with_trace(op, trace).await
|
||||
}
|
||||
|
||||
pub async fn submit_with_trace_and_mcp_app_ui_webview_support(
|
||||
&self,
|
||||
op: Op,
|
||||
trace: Option<W3cTraceContext>,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
) -> CodexResult<String> {
|
||||
self.codex
|
||||
.submit_with_trace_and_mcp_app_ui_webview_support(
|
||||
op,
|
||||
trace,
|
||||
supports_mcp_app_ui_webview,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn submit_user_input_with_client_user_message_id(
|
||||
&self,
|
||||
op: Op,
|
||||
trace: Option<W3cTraceContext>,
|
||||
client_user_message_id: Option<String>,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
) -> CodexResult<String> {
|
||||
self.codex
|
||||
.session
|
||||
@@ -271,7 +287,12 @@ impl CodexThread {
|
||||
.ensure_execution_capacity_for_op(self.session_configured.thread_id, &op)
|
||||
.await?;
|
||||
self.codex
|
||||
.submit_user_input_with_client_user_message_id(op, trace, client_user_message_id)
|
||||
.submit_user_input_with_client_user_message_id(
|
||||
op,
|
||||
trace,
|
||||
client_user_message_id,
|
||||
supports_mcp_app_ui_webview,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -335,12 +356,14 @@ impl CodexThread {
|
||||
&self,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
supports_mcp_app_ui_webview: Option<bool>,
|
||||
mcp_elicitations_auto_deny: bool,
|
||||
) -> ConstraintResult<()> {
|
||||
self.codex
|
||||
.set_app_server_client_info(
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
supports_mcp_app_ui_webview,
|
||||
mcp_elicitations_auto_deny,
|
||||
)
|
||||
.await
|
||||
@@ -614,6 +637,25 @@ impl CodexThread {
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Captures the MCP runtime for one app-server request before detached work begins.
|
||||
pub async fn current_mcp_runtime_with_mcp_app_ui_webview_support(
|
||||
&self,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
) -> CodexResult<Arc<crate::session::McpRuntimeSnapshot>> {
|
||||
let turn_context = self
|
||||
.codex
|
||||
.session
|
||||
.new_default_turn_with_mcp_app_ui_webview_support(supports_mcp_app_ui_webview)
|
||||
.await?;
|
||||
Ok(self
|
||||
.codex
|
||||
.session
|
||||
.capture_step_context(turn_context)
|
||||
.await
|
||||
.mcp
|
||||
.clone())
|
||||
}
|
||||
|
||||
pub fn multi_agent_version(&self) -> Option<MultiAgentVersion> {
|
||||
self.codex.session.multi_agent_version()
|
||||
}
|
||||
|
||||
@@ -967,6 +967,10 @@ pub struct Config {
|
||||
|
||||
/// Optional product SKU forwarded to the host-owned apps MCP server.
|
||||
pub apps_mcp_product_sku: Option<String>,
|
||||
/// Whether the active app-server host can render MCP App WebViews.
|
||||
///
|
||||
/// This is runtime-only state, not a config.toml setting.
|
||||
pub supports_mcp_app_ui_webview: bool,
|
||||
|
||||
/// Machine-local realtime audio device preferences used by realtime voice.
|
||||
pub realtime_audio: RealtimeAudioConfig,
|
||||
@@ -1584,6 +1588,7 @@ impl Config {
|
||||
McpConfig {
|
||||
chatgpt_base_url: self.chatgpt_base_url.clone(),
|
||||
apps_mcp_product_sku: self.apps_mcp_product_sku.clone(),
|
||||
supports_mcp_app_ui_webview: self.supports_mcp_app_ui_webview,
|
||||
codex_home: self.codex_home.to_path_buf(),
|
||||
mcp_oauth_credentials_store_mode: self.mcp_oauth_credentials_store_mode,
|
||||
auth_keyring_backend_kind: self.auth_keyring_backend_kind(),
|
||||
@@ -3908,6 +3913,7 @@ impl Config {
|
||||
.unwrap_or("https://chatgpt.com/backend-api/".to_string()),
|
||||
respect_system_proxy,
|
||||
apps_mcp_product_sku: cfg.apps_mcp_product_sku.clone(),
|
||||
supports_mcp_app_ui_webview: false,
|
||||
realtime_audio: cfg
|
||||
.audio
|
||||
.map_or_else(RealtimeAudioConfig::default, |audio| RealtimeAudioConfig {
|
||||
|
||||
@@ -266,6 +266,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_mcp_manager(
|
||||
codex_apps_tools_cache_key(auth.as_ref()),
|
||||
mcp_config.prefix_mcp_tool_names,
|
||||
mcp_config.client_elicitation_capability,
|
||||
mcp_config.supports_mcp_app_ui_webview,
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
ToolPluginProvenance::default(),
|
||||
auth.as_ref(),
|
||||
|
||||
@@ -1435,6 +1435,7 @@ async fn host_owned_codex_apps_manager(
|
||||
codex_mcp::codex_apps_tools_cache_key(auth.as_ref()),
|
||||
turn_context.config.prefix_mcp_tool_names(),
|
||||
rmcp::model::ElicitationCapability::default(),
|
||||
turn_context.config.supports_mcp_app_ui_webview,
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
codex_mcp::ToolPluginProvenance::default(),
|
||||
auth.as_ref(),
|
||||
|
||||
@@ -700,6 +700,7 @@ pub(crate) async fn handle_start(
|
||||
sess: &Arc<Session>,
|
||||
sub_id: String,
|
||||
params: ConversationStartParams,
|
||||
supports_mcp_app_ui_webview: Option<bool>,
|
||||
) -> CodexResult<()> {
|
||||
let prepared_start = match prepare_realtime_start(sess, params).await {
|
||||
Ok(prepared_start) => prepared_start,
|
||||
@@ -717,7 +718,9 @@ pub(crate) async fn handle_start(
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = handle_start_inner(sess, &sub_id, prepared_start).await {
|
||||
if let Err(err) =
|
||||
handle_start_inner(sess, &sub_id, prepared_start, supports_mcp_app_ui_webview).await
|
||||
{
|
||||
error!("failed to start realtime conversation: {err}");
|
||||
let message = err.to_string();
|
||||
sess.send_event_raw(Event {
|
||||
@@ -982,6 +985,7 @@ async fn handle_start_inner(
|
||||
sess: &Arc<Session>,
|
||||
sub_id: &str,
|
||||
prepared_start: PreparedRealtimeConversationStart,
|
||||
supports_mcp_app_ui_webview: Option<bool>,
|
||||
) -> CodexResult<()> {
|
||||
let PreparedRealtimeConversationStart {
|
||||
api_provider,
|
||||
@@ -1074,7 +1078,9 @@ async fn handle_start_inner(
|
||||
if let Some(text) = maybe_routed_text {
|
||||
debug!(text = %text, "[realtime-text] realtime conversation text output");
|
||||
let sess_for_routed_text = Arc::clone(&sess_clone);
|
||||
sess_for_routed_text.route_realtime_text_input(text).await;
|
||||
sess_for_routed_text
|
||||
.route_realtime_text_input(text, supports_mcp_app_ui_webview)
|
||||
.await;
|
||||
}
|
||||
sess_clone
|
||||
.send_event_raw(ev(EventMsg::RealtimeConversationRealtime(
|
||||
@@ -1085,7 +1091,9 @@ async fn handle_start_inner(
|
||||
.await;
|
||||
}
|
||||
if let Ok(text) = transcript_tail_rx.recv().await {
|
||||
sess_clone.route_realtime_text_input(text).await;
|
||||
sess_clone
|
||||
.route_realtime_text_input(text, supports_mcp_app_ui_webview)
|
||||
.await;
|
||||
}
|
||||
if fanout_realtime_active.swap(false, Ordering::Relaxed) {
|
||||
match end {
|
||||
|
||||
@@ -85,8 +85,16 @@ pub async fn user_input_or_turn(
|
||||
sub_id: String,
|
||||
op: Op,
|
||||
client_user_message_id: Option<String>,
|
||||
supports_mcp_app_ui_webview: Option<bool>,
|
||||
) {
|
||||
user_input_or_turn_inner(sess, sub_id, op, client_user_message_id).await;
|
||||
user_input_or_turn_inner(
|
||||
sess,
|
||||
sub_id,
|
||||
op,
|
||||
client_user_message_id,
|
||||
supports_mcp_app_ui_webview,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn update_thread_settings(
|
||||
@@ -185,6 +193,7 @@ pub(super) async fn user_input_or_turn_inner(
|
||||
sub_id: String,
|
||||
op: Op,
|
||||
client_user_message_id: Option<String>,
|
||||
supports_mcp_app_ui_webview: Option<bool>,
|
||||
) {
|
||||
let Op::UserInput {
|
||||
items,
|
||||
@@ -203,6 +212,7 @@ pub(super) async fn user_input_or_turn_inner(
|
||||
SessionSettingsUpdate::default()
|
||||
};
|
||||
updates.final_output_json_schema = Some(final_output_json_schema);
|
||||
updates.supports_mcp_app_ui_webview = supports_mcp_app_ui_webview;
|
||||
|
||||
let Ok(current_context) = sess.new_turn_with_sub_id(sub_id.clone(), updates).await else {
|
||||
// new_turn_with_sub_id already emits the error event.
|
||||
@@ -669,8 +679,26 @@ pub async fn review(
|
||||
config: &Arc<Config>,
|
||||
sub_id: String,
|
||||
review_request: ReviewRequest,
|
||||
supports_mcp_app_ui_webview: Option<bool>,
|
||||
) {
|
||||
let turn_context = sess.new_default_turn_with_sub_id(sub_id.clone()).await;
|
||||
let turn_context = match supports_mcp_app_ui_webview {
|
||||
Some(supports_mcp_app_ui_webview) => {
|
||||
let Ok(turn_context) = sess
|
||||
.new_turn_with_sub_id(
|
||||
sub_id.clone(),
|
||||
SessionSettingsUpdate {
|
||||
supports_mcp_app_ui_webview: Some(supports_mcp_app_ui_webview),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
turn_context
|
||||
}
|
||||
None => sess.new_default_turn_with_sub_id(sub_id.clone()).await,
|
||||
};
|
||||
sess.maybe_emit_model_warnings_for_turn(turn_context.as_ref())
|
||||
.await;
|
||||
sess.refresh_mcp_servers_if_requested(&turn_context, Some(sess.mcp_elicitation_reviewer()))
|
||||
@@ -721,8 +749,13 @@ pub(super) async fn submission_loop(
|
||||
false
|
||||
}
|
||||
Op::RealtimeConversationStart(params) => {
|
||||
if let Err(err) =
|
||||
handle_realtime_conversation_start(&sess, sub.id.clone(), params).await
|
||||
if let Err(err) = handle_realtime_conversation_start(
|
||||
&sess,
|
||||
sub.id.clone(),
|
||||
params,
|
||||
sub.supports_mcp_app_ui_webview,
|
||||
)
|
||||
.await
|
||||
{
|
||||
sess.send_event_raw(Event {
|
||||
id: sub.id.clone(),
|
||||
@@ -756,8 +789,14 @@ pub(super) async fn submission_loop(
|
||||
false
|
||||
}
|
||||
Op::UserInput { .. } => {
|
||||
user_input_or_turn(&sess, sub.id.clone(), sub.op, sub.client_user_message_id)
|
||||
.await;
|
||||
user_input_or_turn(
|
||||
&sess,
|
||||
sub.id.clone(),
|
||||
sub.op,
|
||||
sub.client_user_message_id,
|
||||
sub.supports_mcp_app_ui_webview,
|
||||
)
|
||||
.await;
|
||||
false
|
||||
}
|
||||
Op::ThreadSettings { thread_settings } => {
|
||||
@@ -829,7 +868,14 @@ pub(super) async fn submission_loop(
|
||||
}
|
||||
Op::Shutdown => shutdown(&sess, sub.id.clone()).await,
|
||||
Op::Review { review_request } => {
|
||||
review(&sess, &config, sub.id.clone(), review_request).await;
|
||||
review(
|
||||
&sess,
|
||||
&config,
|
||||
sub.id.clone(),
|
||||
review_request,
|
||||
sub.supports_mcp_app_ui_webview,
|
||||
)
|
||||
.await;
|
||||
false
|
||||
}
|
||||
Op::ApproveGuardianDeniedAction { event } => {
|
||||
|
||||
@@ -116,13 +116,21 @@ impl Session {
|
||||
let available_environment_ids =
|
||||
Self::available_selected_environment_ids(selected_capability_roots);
|
||||
let current = self.services.latest_mcp_runtime();
|
||||
if current.available_environment_ids() == available_environment_ids {
|
||||
let host_capability_matches = current.config().supports_mcp_app_ui_webview
|
||||
== turn_context.config.supports_mcp_app_ui_webview;
|
||||
if current.available_environment_ids() == available_environment_ids
|
||||
&& host_capability_matches
|
||||
{
|
||||
return current;
|
||||
}
|
||||
|
||||
let _guard = self.services.mcp_projection_lock.lock().await;
|
||||
let current = self.services.latest_mcp_runtime();
|
||||
if current.available_environment_ids() == available_environment_ids {
|
||||
let host_capability_matches = current.config().supports_mcp_app_ui_webview
|
||||
== turn_context.config.supports_mcp_app_ui_webview;
|
||||
if current.available_environment_ids() == available_environment_ids
|
||||
&& host_capability_matches
|
||||
{
|
||||
return current;
|
||||
}
|
||||
let mcp_projection = self
|
||||
@@ -153,6 +161,8 @@ impl Session {
|
||||
.mcp_server_catalog
|
||||
.has_same_servers(&mcp_config.mcp_server_catalog)
|
||||
&& current.config().connector_snapshot == mcp_config.connector_snapshot
|
||||
&& current.config().supports_mcp_app_ui_webview
|
||||
== mcp_config.supports_mcp_app_ui_webview
|
||||
{
|
||||
// Availability is only an input to the MCP projection. When that input changes but
|
||||
// the projected servers and connectors do not, advance the input key without
|
||||
@@ -377,6 +387,7 @@ impl Session {
|
||||
codex_apps_tools_cache_key(auth.as_ref()),
|
||||
mcp_config.prefix_mcp_tool_names,
|
||||
mcp_config.client_elicitation_capability.clone(),
|
||||
mcp_config.supports_mcp_app_ui_webview,
|
||||
self.services
|
||||
.supports_openai_form_elicitation
|
||||
.load(std::sync::atomic::Ordering::Relaxed),
|
||||
|
||||
@@ -79,6 +79,7 @@ impl McpRuntimeSnapshot {
|
||||
apps_enabled: config.features.enabled(Feature::Apps),
|
||||
prefix_mcp_tool_names: config.prefix_mcp_tool_names(),
|
||||
client_elicitation_capability: ElicitationCapability::default(),
|
||||
supports_mcp_app_ui_webview: config.supports_mcp_app_ui_webview,
|
||||
mcp_server_catalog: ResolvedMcpCatalog::default(),
|
||||
connector_snapshot: codex_connectors::ConnectorSnapshot::default(),
|
||||
};
|
||||
|
||||
@@ -752,6 +752,25 @@ impl Codex {
|
||||
id: id.clone(),
|
||||
op,
|
||||
client_user_message_id: None,
|
||||
supports_mcp_app_ui_webview: None,
|
||||
trace,
|
||||
};
|
||||
self.submit_with_id(sub).await?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub async fn submit_with_trace_and_mcp_app_ui_webview_support(
|
||||
&self,
|
||||
op: Op,
|
||||
trace: Option<W3cTraceContext>,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
) -> CodexResult<String> {
|
||||
let id = new_submission_id();
|
||||
let sub = Submission {
|
||||
id: id.clone(),
|
||||
op,
|
||||
client_user_message_id: None,
|
||||
supports_mcp_app_ui_webview: Some(supports_mcp_app_ui_webview),
|
||||
trace,
|
||||
};
|
||||
self.submit_with_id(sub).await?;
|
||||
@@ -763,6 +782,7 @@ impl Codex {
|
||||
op: Op,
|
||||
trace: Option<W3cTraceContext>,
|
||||
client_user_message_id: Option<String>,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
) -> CodexResult<String> {
|
||||
debug_assert!(matches!(op, Op::UserInput { .. }));
|
||||
let id = new_submission_id();
|
||||
@@ -770,6 +790,7 @@ impl Codex {
|
||||
id: id.clone(),
|
||||
op,
|
||||
client_user_message_id,
|
||||
supports_mcp_app_ui_webview: Some(supports_mcp_app_ui_webview),
|
||||
trace,
|
||||
};
|
||||
self.submit_with_id(sub).await?;
|
||||
@@ -843,12 +864,14 @@ impl Codex {
|
||||
&self,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
supports_mcp_app_ui_webview: Option<bool>,
|
||||
mcp_elicitations_auto_deny: bool,
|
||||
) -> ConstraintResult<()> {
|
||||
self.session
|
||||
.update_settings(SessionSettingsUpdate {
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
supports_mcp_app_ui_webview,
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
@@ -1220,7 +1243,11 @@ impl Session {
|
||||
format!("auto-compact-{id}")
|
||||
}
|
||||
|
||||
pub(crate) async fn route_realtime_text_input(self: &Arc<Self>, text: String) {
|
||||
pub(crate) async fn route_realtime_text_input(
|
||||
self: &Arc<Self>,
|
||||
text: String,
|
||||
supports_mcp_app_ui_webview: Option<bool>,
|
||||
) {
|
||||
handlers::user_input_or_turn_inner(
|
||||
self,
|
||||
Uuid::now_v7().to_string(),
|
||||
@@ -1235,6 +1262,7 @@ impl Session {
|
||||
thread_settings: Default::default(),
|
||||
},
|
||||
/*client_user_message_id*/ None,
|
||||
supports_mcp_app_ui_webview,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -373,6 +373,11 @@ impl SessionConfiguration {
|
||||
if let Some(app_server_client_version) = updates.app_server_client_version.clone() {
|
||||
next_configuration.app_server_client_version = Some(app_server_client_version);
|
||||
}
|
||||
if let Some(supports_mcp_app_ui_webview) = updates.supports_mcp_app_ui_webview {
|
||||
let mut config = (*next_configuration.original_config_do_not_use).clone();
|
||||
config.supports_mcp_app_ui_webview = supports_mcp_app_ui_webview;
|
||||
next_configuration.original_config_do_not_use = Arc::new(config);
|
||||
}
|
||||
Ok(next_configuration)
|
||||
}
|
||||
|
||||
@@ -431,6 +436,7 @@ pub(crate) struct SessionSettingsUpdate {
|
||||
pub(crate) personality: Option<Personality>,
|
||||
pub(crate) app_server_client_name: Option<String>,
|
||||
pub(crate) app_server_client_version: Option<String>,
|
||||
pub(crate) supports_mcp_app_ui_webview: Option<bool>,
|
||||
}
|
||||
|
||||
pub(crate) struct AppServerClientMetadata {
|
||||
@@ -1213,6 +1219,7 @@ impl Session {
|
||||
.config
|
||||
.client_elicitation_capability
|
||||
.clone(),
|
||||
mcp_projection.config.supports_mcp_app_ui_webview,
|
||||
sess.services
|
||||
.supports_openai_form_elicitation
|
||||
.load(std::sync::atomic::Ordering::Relaxed),
|
||||
|
||||
@@ -6479,6 +6479,7 @@ async fn submit_with_id_captures_current_span_trace_context() {
|
||||
id: "sub-1".into(),
|
||||
op: Op::Interrupt,
|
||||
client_user_message_id: None,
|
||||
supports_mcp_app_ui_webview: None,
|
||||
trace: None,
|
||||
})
|
||||
.await
|
||||
@@ -6492,6 +6493,40 @@ async fn submit_with_id_captures_current_span_trace_context() {
|
||||
assert_eq!(submitted.trace, Some(expected_trace));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn user_input_submission_carries_request_scoped_mcp_app_ui_capability() {
|
||||
let (session, _turn_context) = make_session_and_context().await;
|
||||
let (tx_sub, rx_sub) = async_channel::bounded(1);
|
||||
let (_tx_event, rx_event) = async_channel::unbounded();
|
||||
let (_agent_status_tx, agent_status) = watch::channel(AgentStatus::PendingInit);
|
||||
let codex = Codex {
|
||||
tx_sub,
|
||||
rx_event,
|
||||
agent_status,
|
||||
session: Arc::new(session),
|
||||
session_loop_termination: completed_session_loop_termination(),
|
||||
};
|
||||
|
||||
codex
|
||||
.submit_user_input_with_client_user_message_id(
|
||||
Op::UserInput {
|
||||
items: Vec::new(),
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
},
|
||||
/*trace*/ None,
|
||||
/*client_user_message_id*/ None,
|
||||
/*supports_mcp_app_ui_webview*/ true,
|
||||
)
|
||||
.await
|
||||
.expect("submit user input");
|
||||
|
||||
let submitted = rx_sub.recv().await.expect("submission");
|
||||
assert_eq!(submitted.supports_mcp_app_ui_webview, Some(true));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn new_default_turn_captures_current_span_trace_id() {
|
||||
let (session, _turn_context) = make_session_and_context().await;
|
||||
@@ -6551,6 +6586,7 @@ fn submission_dispatch_span_prefers_submission_trace_context() {
|
||||
id: "sub-1".into(),
|
||||
op: Op::Interrupt,
|
||||
client_user_message_id: None,
|
||||
supports_mcp_app_ui_webview: None,
|
||||
trace: Some(submission_trace),
|
||||
})
|
||||
});
|
||||
@@ -6578,6 +6614,7 @@ fn submission_dispatch_span_uses_debug_for_realtime_audio() {
|
||||
},
|
||||
}),
|
||||
client_user_message_id: None,
|
||||
supports_mcp_app_ui_webview: None,
|
||||
trace: None,
|
||||
});
|
||||
|
||||
@@ -6644,6 +6681,7 @@ async fn user_turn_updates_approvals_reviewer() {
|
||||
},
|
||||
},
|
||||
/*client_user_message_id*/ None,
|
||||
/*supports_mcp_app_ui_webview*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -6654,6 +6692,27 @@ async fn user_turn_updates_approvals_reviewer() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn routed_realtime_text_uses_the_starting_client_mcp_app_ui_capability() {
|
||||
let (session, _turn_context, _rx) = make_session_and_context_with_rx().await;
|
||||
session
|
||||
.update_settings(SessionSettingsUpdate {
|
||||
supports_mcp_app_ui_webview: Some(true),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.expect("simulate a later widget-capable client update");
|
||||
|
||||
session
|
||||
.route_realtime_text_input(
|
||||
"hello from realtime".to_string(),
|
||||
/*supports_mcp_app_ui_webview*/ Some(false),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(!session.get_config().await.supports_mcp_app_ui_webview);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_environments_set_primary_environment() {
|
||||
let (session, _turn_context, _rx) = make_session_and_context_with_rx().await;
|
||||
@@ -6891,6 +6950,7 @@ async fn spawn_task_turn_span_inherits_dispatch_trace_context() {
|
||||
id: "sub-1".into(),
|
||||
op: Op::Interrupt,
|
||||
client_user_message_id: None,
|
||||
supports_mcp_app_ui_webview: None,
|
||||
trace: Some(submission_trace.clone()),
|
||||
});
|
||||
let dispatch_span_id = dispatch_span.context().span().span_context().span_id();
|
||||
@@ -7759,6 +7819,65 @@ async fn refresh_mcp_servers_keeps_the_previous_runtime_alive() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn host_mcp_app_ui_capability_change_rebuilds_the_mcp_manager() {
|
||||
let (session, _) = make_session_and_context().await;
|
||||
let session = Arc::new(session);
|
||||
let old_runtime = session.services.latest_mcp_runtime();
|
||||
let old_manager = old_runtime.manager_arc();
|
||||
assert!(!old_runtime.config().supports_mcp_app_ui_webview);
|
||||
|
||||
session
|
||||
.update_settings(SessionSettingsUpdate {
|
||||
supports_mcp_app_ui_webview: Some(true),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.expect("update host MCP App UI capability");
|
||||
let turn_context = session.new_default_turn().await;
|
||||
|
||||
let new_runtime = session
|
||||
.mcp_runtime_for_step(
|
||||
turn_context.as_ref(),
|
||||
&turn_context.environments,
|
||||
/*selected_capability_roots*/ &[],
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(new_runtime.config().supports_mcp_app_ui_webview);
|
||||
assert!(!Arc::ptr_eq(&old_runtime, &new_runtime));
|
||||
assert!(!Arc::ptr_eq(&old_manager, &new_runtime.manager_arc()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn request_scoped_host_mcp_app_ui_capability_survives_later_session_update() {
|
||||
let (session, _) = make_session_and_context().await;
|
||||
let session = Arc::new(session);
|
||||
let widget_turn_context = session
|
||||
.new_default_turn_with_mcp_app_ui_webview_support(true)
|
||||
.await
|
||||
.expect("capture widget client turn context");
|
||||
|
||||
session
|
||||
.update_settings(SessionSettingsUpdate {
|
||||
supports_mcp_app_ui_webview: Some(false),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.expect("simulate a later text-only client update");
|
||||
|
||||
let widget_runtime = session
|
||||
.mcp_runtime_for_step(
|
||||
widget_turn_context.as_ref(),
|
||||
&widget_turn_context.environments,
|
||||
/*selected_capability_roots*/ &[],
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(widget_turn_context.config.supports_mcp_app_ui_webview);
|
||||
assert!(widget_runtime.config().supports_mcp_app_ui_webview);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_availability_change_reuses_the_mcp_manager() {
|
||||
struct ReadyPluginContributor;
|
||||
|
||||
@@ -816,6 +816,20 @@ impl Session {
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn new_default_turn_with_mcp_app_ui_webview_support(
|
||||
&self,
|
||||
supports_mcp_app_ui_webview: bool,
|
||||
) -> CodexResult<Arc<TurnContext>> {
|
||||
self.new_turn_with_sub_id(
|
||||
self.next_internal_sub_id(),
|
||||
SessionSettingsUpdate {
|
||||
supports_mcp_app_ui_webview: Some(supports_mcp_app_ui_webview),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn new_startup_prewarm_turn_with_sub_id(
|
||||
&self,
|
||||
sub_id: String,
|
||||
|
||||
@@ -124,6 +124,71 @@ async fn submit_user_turn(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn codex_apps_tool_call_forwards_mcp_app_ui_webview_capability() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let apps_server = AppsTestServer::mount(&server).await?;
|
||||
let call_id = "calendar-call-with-ui-capability";
|
||||
let calendar_args = serde_json::to_string(&json!({
|
||||
"title": "Lunch",
|
||||
"starts_at": "2026-03-10T12:00:00Z"
|
||||
}))?;
|
||||
let mock = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_function_call_with_namespace(
|
||||
call_id,
|
||||
SEARCH_CALENDAR_NAMESPACE,
|
||||
SEARCH_CALENDAR_CREATE_TOOL,
|
||||
&calendar_args,
|
||||
),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-2"),
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed("resp-2"),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut builder = search_capable_apps_builder(apps_server.chatgpt_base_url.clone())
|
||||
.with_config(|config| {
|
||||
config.supports_mcp_app_ui_webview = true;
|
||||
set_calendar_approval_mode(config, AppToolApproval::Auto);
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
submit_user_turn(
|
||||
&test,
|
||||
"Use [$calendar](app://calendar) to create a calendar event.",
|
||||
AskForApproval::Never,
|
||||
/*collaboration_mode*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(mock.requests().len(), 2);
|
||||
let apps_tool_call = recorded_apps_tool_call_by_call_id(&server, call_id).await;
|
||||
assert_eq!(
|
||||
apps_tool_call.pointer(
|
||||
"/params/_meta/io.modelcontextprotocol~1clientCapabilities/extensions/io.modelcontextprotocol~1ui/mimeTypes"
|
||||
),
|
||||
Some(&json!(["text/html;profile=mcp-app"]))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn approved_mcp_tool_call_metadata_records_prior_user_input_request() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
@@ -114,6 +114,7 @@ pub async fn run_codex_tool_session(
|
||||
thread_settings: Default::default(),
|
||||
},
|
||||
client_user_message_id: None,
|
||||
supports_mcp_app_ui_webview: None,
|
||||
trace: None,
|
||||
};
|
||||
|
||||
|
||||
@@ -546,6 +546,7 @@ impl MessageProcessor {
|
||||
id: request_id_string,
|
||||
op: codex_protocol::protocol::Op::Interrupt,
|
||||
client_user_message_id: None,
|
||||
supports_mcp_app_ui_webview: None,
|
||||
trace: None,
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -166,6 +166,11 @@ pub struct Submission {
|
||||
pub op: Op,
|
||||
/// Client-provided id for the user message represented by `Op::UserInput`.
|
||||
pub client_user_message_id: Option<String>,
|
||||
/// Request-scoped host capability for turns submitted by app-server clients.
|
||||
///
|
||||
/// This is carried with the queued operation so a later request on the same
|
||||
/// thread cannot change which MCP runtime the turn initializes.
|
||||
pub supports_mcp_app_ui_webview: Option<bool>,
|
||||
/// Optional W3C trace carrier propagated across async submission handoffs.
|
||||
pub trace: Option<W3cTraceContext>,
|
||||
}
|
||||
|
||||
@@ -261,6 +261,7 @@ fn new_config(model: Option<String>, arg0_paths: Arg0DispatchPaths) -> anyhow::R
|
||||
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
|
||||
respect_system_proxy: false,
|
||||
apps_mcp_product_sku: None,
|
||||
supports_mcp_app_ui_webview: false,
|
||||
realtime_audio: RealtimeAudioConfig::default(),
|
||||
experimental_realtime_ws_base_url: None,
|
||||
experimental_realtime_webrtc_call_base_url: None,
|
||||
|
||||
Reference in New Issue
Block a user