mirror of
https://github.com/openai/codex.git
synced 2026-10-28 00:03:58 +00:00
Negotiate MCP extensions per app-server session (#36910)
## Why App-server clients need to advertise structured MCP extension settings, including supported MCP App UI MIME types, rather than only opting into OpenAI form elicitation with a boolean. ## What changed - Add an `extensions` map to initialize capabilities and preserve `mcpServerOpenaiFormElicitation` as a legacy alias for `openai/form`. - Capture the declared extension profile when a thread is started, resumed, or forked, propagate it to subagents, and advertise it to downstream MCP servers during initialization. - Keep the profile stable for the lifetime of the loaded session instead of changing it on later turns or direct tool calls. ## Testing - Cover extension conversion, downstream MCP initialization, session isolation, legacy form support, and subagent inheritance. GitOrigin-RevId: fbcedbb74ce788e574b0f884a4c45c4cedb9de54
This commit is contained in:
committed by
copyberry
parent
bab7c2dcc1
commit
ee46c5ba0e
@@ -132,10 +132,7 @@ pub(crate) async fn run_codex_thread_interactive(
|
||||
parent_trace: None,
|
||||
environment_selections: parent_ctx.environments.to_selections(),
|
||||
thread_extension_init: codex_extension_api::ExtensionDataInit::default(),
|
||||
supports_openai_form_elicitation: parent_session
|
||||
.services
|
||||
.supports_openai_form_elicitation
|
||||
.load(std::sync::atomic::Ordering::Relaxed),
|
||||
client_mcp_extensions: parent_session.services.client_mcp_extensions.clone(),
|
||||
analytics_events_client: Some(parent_session.services.analytics_events_client.clone()),
|
||||
thread_store: Arc::clone(&parent_session.services.thread_store),
|
||||
attestation_provider: parent_session.services.attestation_provider.clone(),
|
||||
|
||||
@@ -406,12 +406,6 @@ impl CodexThread {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn set_openai_form_elicitation_support(&self, supported: bool) -> anyhow::Result<()> {
|
||||
self.session
|
||||
.set_openai_form_elicitation_support(supported)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Preview persistent thread settings overrides without committing them.
|
||||
pub async fn preview_thread_settings_overrides(
|
||||
&self,
|
||||
|
||||
@@ -39,6 +39,7 @@ use codex_mcp::ToolInfo;
|
||||
use codex_mcp::ToolPluginProvenance;
|
||||
use codex_mcp::effective_mcp_servers;
|
||||
use codex_mcp::tool_plugin_provenance;
|
||||
use codex_protocol::mcp::ClientMcpExtensions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
|
||||
const CONNECTORS_READY_TIMEOUT_ON_EMPTY_TOOLS: Duration = Duration::from_secs(30);
|
||||
@@ -259,7 +260,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_mcp_manager(
|
||||
codex_apps_tools_cache: mcp_manager.codex_apps_tools_cache(),
|
||||
tool_catalog_cache: mcp_manager.tool_catalog_cache(),
|
||||
codex_apps_tools_cache_key: connector_runtime_context_key(auth.as_ref()),
|
||||
supports_openai_form_elicitation: false,
|
||||
client_mcp_extensions: ClientMcpExtensions::default(),
|
||||
auth: auth.clone(),
|
||||
codex_apps_auth_manager,
|
||||
elicitation_reviewer: None,
|
||||
|
||||
@@ -558,26 +558,6 @@ impl Session {
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn set_openai_form_elicitation_support(
|
||||
&self,
|
||||
supported: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
if self
|
||||
.services
|
||||
.supports_openai_form_elicitation
|
||||
.load(std::sync::atomic::Ordering::Relaxed)
|
||||
== supported
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.services
|
||||
.supports_openai_form_elicitation
|
||||
.store(supported, std::sync::atomic::Ordering::Relaxed);
|
||||
self.request_mcp_runtime_refresh();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn refresh_mcp_servers_now(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
|
||||
@@ -138,10 +138,6 @@ impl Session {
|
||||
elicitation_reviewer: Option<ElicitationReviewerHandle>,
|
||||
) -> McpRuntimeInput {
|
||||
let auth = desired.auth.clone();
|
||||
let supports_openai_form_elicitation = self
|
||||
.services
|
||||
.supports_openai_form_elicitation
|
||||
.load(std::sync::atomic::Ordering::Acquire);
|
||||
let McpRuntimeProjection {
|
||||
mut config,
|
||||
plugins_available,
|
||||
@@ -186,7 +182,7 @@ impl Session {
|
||||
codex_apps_tools_cache: self.services.mcp_manager.codex_apps_tools_cache(),
|
||||
tool_catalog_cache: self.services.mcp_manager.tool_catalog_cache(),
|
||||
codex_apps_tools_cache_key: connector_runtime_context_key(auth.as_ref()),
|
||||
supports_openai_form_elicitation,
|
||||
client_mcp_extensions: self.services.client_mcp_extensions.clone(),
|
||||
auth,
|
||||
codex_apps_auth_manager,
|
||||
elicitation_reviewer,
|
||||
|
||||
@@ -340,6 +340,7 @@ use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::mcp::ClientMcpExtensions;
|
||||
use codex_protocol::models::LocalImagePreparation;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
@@ -448,7 +449,7 @@ pub(crate) struct SessionSpawnArgs {
|
||||
pub(crate) parent_trace: Option<W3cTraceContext>,
|
||||
pub(crate) environment_selections: Vec<TurnEnvironmentSelection>,
|
||||
pub(crate) thread_extension_init: ExtensionDataInit,
|
||||
pub(crate) supports_openai_form_elicitation: bool,
|
||||
pub(crate) client_mcp_extensions: ClientMcpExtensions,
|
||||
pub(crate) analytics_events_client: Option<AnalyticsEventsClient>,
|
||||
pub(crate) thread_store: Arc<dyn ThreadStore>,
|
||||
pub(crate) attestation_provider: Option<Arc<dyn AttestationProvider>>,
|
||||
@@ -540,7 +541,7 @@ impl Session {
|
||||
parent_trace: _,
|
||||
environment_selections,
|
||||
thread_extension_init,
|
||||
supports_openai_form_elicitation,
|
||||
client_mcp_extensions,
|
||||
analytics_events_client,
|
||||
thread_store,
|
||||
attestation_provider,
|
||||
@@ -731,7 +732,7 @@ impl Session {
|
||||
code_mode_session_provider,
|
||||
extensions,
|
||||
thread_extension_init,
|
||||
supports_openai_form_elicitation,
|
||||
client_mcp_extensions,
|
||||
agent_control,
|
||||
environment_manager,
|
||||
inherited_environments,
|
||||
|
||||
@@ -17,6 +17,7 @@ use codex_protocol::SessionId;
|
||||
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::mcp::ClientMcpExtensions;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
@@ -515,7 +516,7 @@ impl Session {
|
||||
code_mode_session_provider: Arc<dyn codex_code_mode::CodeModeSessionProvider>,
|
||||
extensions: Arc<codex_extension_api::ExtensionRegistry<crate::config::Config>>,
|
||||
mut thread_extension_init: ExtensionDataInit,
|
||||
supports_openai_form_elicitation: bool,
|
||||
client_mcp_extensions: ClientMcpExtensions,
|
||||
agent_control: AgentControl,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
inherited_environments: Option<TurnEnvironmentSnapshot>,
|
||||
@@ -1126,9 +1127,7 @@ impl Session {
|
||||
thread_extension_data,
|
||||
selected_capability_roots,
|
||||
mcp_thread_init,
|
||||
supports_openai_form_elicitation: std::sync::atomic::AtomicBool::new(
|
||||
supports_openai_form_elicitation,
|
||||
),
|
||||
client_mcp_extensions,
|
||||
agent_control,
|
||||
network_proxy: arc_swap::ArcSwapOption::from(network_proxy.map(Arc::new)),
|
||||
network_proxy_audit_metadata,
|
||||
|
||||
@@ -5546,7 +5546,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
|
||||
Arc::new(codex_code_mode::DisabledCodeModeSessionProvider),
|
||||
Arc::new(codex_extension_api::ExtensionRegistryBuilder::new().build()),
|
||||
codex_extension_api::ExtensionDataInit::default(),
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
ClientMcpExtensions::default(),
|
||||
AgentControl::default(),
|
||||
environment_manager,
|
||||
/*inherited_environments*/ None,
|
||||
@@ -5742,7 +5742,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
thread_extension_data: codex_extension_api::ExtensionData::new(thread_id.to_string()),
|
||||
selected_capability_roots: Vec::new(),
|
||||
mcp_thread_init: codex_extension_api::ExtensionDataInit::default(),
|
||||
supports_openai_form_elicitation: std::sync::atomic::AtomicBool::new(false),
|
||||
client_mcp_extensions: ClientMcpExtensions::default(),
|
||||
agent_control,
|
||||
network_proxy: arc_swap::ArcSwapOption::from(None),
|
||||
network_proxy_audit_metadata: crate::config::NetworkProxyAuditMetadata::default(),
|
||||
@@ -5956,7 +5956,7 @@ async fn make_session_with_config_and_rx(
|
||||
Arc::new(codex_code_mode::DisabledCodeModeSessionProvider),
|
||||
Arc::new(codex_extension_api::ExtensionRegistryBuilder::new().build()),
|
||||
codex_extension_api::ExtensionDataInit::default(),
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
ClientMcpExtensions::default(),
|
||||
AgentControl::default(),
|
||||
environment_manager,
|
||||
/*inherited_environments*/ None,
|
||||
@@ -6066,7 +6066,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
|
||||
Arc::new(codex_code_mode::DisabledCodeModeSessionProvider),
|
||||
Arc::new(codex_extension_api::ExtensionRegistryBuilder::new().build()),
|
||||
codex_extension_api::ExtensionDataInit::default(),
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
ClientMcpExtensions::default(),
|
||||
agent_control,
|
||||
environment_manager,
|
||||
/*inherited_environments*/ None,
|
||||
@@ -7936,7 +7936,7 @@ where
|
||||
thread_extension_data: codex_extension_api::ExtensionData::new(thread_id.to_string()),
|
||||
selected_capability_roots: Vec::new(),
|
||||
mcp_thread_init: codex_extension_api::ExtensionDataInit::default(),
|
||||
supports_openai_form_elicitation: std::sync::atomic::AtomicBool::new(false),
|
||||
client_mcp_extensions: ClientMcpExtensions::default(),
|
||||
agent_control,
|
||||
network_proxy: arc_swap::ArcSwapOption::from(None),
|
||||
network_proxy_audit_metadata: crate::config::NetworkProxyAuditMetadata::default(),
|
||||
|
||||
@@ -765,7 +765,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
|
||||
parent_trace: None,
|
||||
environment_selections: Vec::new(),
|
||||
thread_extension_init: codex_extension_api::ExtensionDataInit::default(),
|
||||
supports_openai_form_elicitation: false,
|
||||
client_mcp_extensions: ClientMcpExtensions::default(),
|
||||
analytics_events_client: None,
|
||||
thread_store,
|
||||
attestation_provider: None,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use crate::SkillsService;
|
||||
use crate::agent::AgentControl;
|
||||
@@ -35,6 +34,7 @@ use codex_mcp::McpRuntime;
|
||||
use codex_models_manager::manager::SharedModelsManager;
|
||||
use codex_otel::SessionTelemetry;
|
||||
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
||||
use codex_protocol::mcp::ClientMcpExtensions;
|
||||
use codex_rollout::state_db::StateDbHandle;
|
||||
use codex_rollout_trace::ThreadTraceContext;
|
||||
use codex_thread_store::LiveThread;
|
||||
@@ -72,7 +72,8 @@ pub(crate) struct SessionServices {
|
||||
pub(crate) extensions: Arc<ExtensionRegistry<crate::config::Config>>,
|
||||
pub(crate) session_extension_data: ExtensionData,
|
||||
pub(crate) thread_extension_data: ExtensionData,
|
||||
pub(crate) supports_openai_form_elicitation: AtomicBool,
|
||||
/// MCP extensions fixed when this session is created.
|
||||
pub(crate) client_mcp_extensions: ClientMcpExtensions,
|
||||
/// Raw capability selections for this thread. Each model step resolves them against its
|
||||
/// current executor environments before using them.
|
||||
pub(crate) selected_capability_roots: Vec<SelectedCapabilityRoot>,
|
||||
|
||||
@@ -24,6 +24,8 @@ use codex_models_manager::test_support::construct_model_info_offline_for_tests;
|
||||
use codex_models_manager::test_support::get_model_offline_for_tests;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::CollaborationModeMask;
|
||||
use codex_protocol::mcp::ClientMcpExtensions;
|
||||
use codex_protocol::mcp::OPENAI_FORM_EXTENSION_ID;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
@@ -112,7 +114,10 @@ pub async fn start_thread_with_user_shell_override(
|
||||
.start_thread_with_user_shell_override_for_tests(
|
||||
config,
|
||||
user_shell_override,
|
||||
supports_openai_form_elicitation,
|
||||
ClientMcpExtensions::new(
|
||||
supports_openai_form_elicitation
|
||||
.then(|| (OPENAI_FORM_EXTENSION_ID.to_string(), serde_json::json!({}))),
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -131,7 +136,10 @@ pub async fn resume_thread_from_rollout_with_user_shell_override(
|
||||
rollout_path,
|
||||
auth_manager,
|
||||
user_shell_override,
|
||||
supports_openai_form_elicitation,
|
||||
ClientMcpExtensions::new(
|
||||
supports_openai_form_elicitation
|
||||
.then(|| (OPENAI_FORM_EXTENSION_ID.to_string(), serde_json::json!({}))),
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::CollaborationModeMask;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::Result as CodexResult;
|
||||
use codex_protocol::mcp::ClientMcpExtensions;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::protocol::Event;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
@@ -210,7 +211,7 @@ pub struct StartThreadOptions {
|
||||
pub parent_trace: Option<W3cTraceContext>,
|
||||
pub environments: Option<Vec<TurnEnvironmentSelection>>,
|
||||
pub thread_extension_init: ExtensionDataInit,
|
||||
pub supports_openai_form_elicitation: bool,
|
||||
pub client_mcp_extensions: ClientMcpExtensions,
|
||||
}
|
||||
|
||||
impl StartThreadOptions {
|
||||
@@ -227,7 +228,7 @@ impl StartThreadOptions {
|
||||
parent_trace: None,
|
||||
environments: None,
|
||||
thread_extension_init: ExtensionDataInit::default(),
|
||||
supports_openai_form_elicitation: false,
|
||||
client_mcp_extensions: ClientMcpExtensions::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -873,7 +874,7 @@ impl ThreadManager {
|
||||
rollout_path: PathBuf,
|
||||
auth_manager: Arc<AuthManager>,
|
||||
parent_trace: Option<W3cTraceContext>,
|
||||
supports_openai_form_elicitation: bool,
|
||||
client_mcp_extensions: ClientMcpExtensions,
|
||||
) -> CodexResult<NewThread> {
|
||||
let initial_history = self.initial_history_from_rollout_path(rollout_path).await?;
|
||||
Box::pin(self.resume_thread_with_history(
|
||||
@@ -881,7 +882,7 @@ impl ThreadManager {
|
||||
initial_history,
|
||||
auth_manager,
|
||||
parent_trace,
|
||||
supports_openai_form_elicitation,
|
||||
client_mcp_extensions,
|
||||
))
|
||||
.await
|
||||
}
|
||||
@@ -893,7 +894,7 @@ impl ThreadManager {
|
||||
initial_history: InitialHistory,
|
||||
auth_manager: Arc<AuthManager>,
|
||||
parent_trace: Option<W3cTraceContext>,
|
||||
supports_openai_form_elicitation: bool,
|
||||
client_mcp_extensions: ClientMcpExtensions,
|
||||
) -> CodexResult<NewThread> {
|
||||
let agent_control = self.agent_control_for_config(&config);
|
||||
let (session_source, thread_source) = initial_history
|
||||
@@ -912,7 +913,7 @@ impl ThreadManager {
|
||||
session_source: Some(session_source),
|
||||
thread_source,
|
||||
parent_trace,
|
||||
supports_openai_form_elicitation,
|
||||
client_mcp_extensions,
|
||||
..StartThreadOptions::new(config)
|
||||
};
|
||||
Box::pin(self.state.spawn_thread(ThreadSpawnRequest::new(
|
||||
@@ -927,11 +928,11 @@ impl ThreadManager {
|
||||
&self,
|
||||
config: Config,
|
||||
user_shell_override: crate::shell::Shell,
|
||||
supports_openai_form_elicitation: bool,
|
||||
client_mcp_extensions: ClientMcpExtensions,
|
||||
) -> CodexResult<NewThread> {
|
||||
let agent_control = self.agent_control_for_config(&config);
|
||||
let options = StartThreadOptions {
|
||||
supports_openai_form_elicitation,
|
||||
client_mcp_extensions,
|
||||
..StartThreadOptions::new(config)
|
||||
};
|
||||
let mut request =
|
||||
@@ -946,7 +947,7 @@ impl ThreadManager {
|
||||
rollout_path: PathBuf,
|
||||
auth_manager: Arc<AuthManager>,
|
||||
user_shell_override: crate::shell::Shell,
|
||||
supports_openai_form_elicitation: bool,
|
||||
client_mcp_extensions: ClientMcpExtensions,
|
||||
) -> CodexResult<NewThread> {
|
||||
let agent_control = self.agent_control_for_config(&config);
|
||||
let initial_history = self.initial_history_from_rollout_path(rollout_path).await?;
|
||||
@@ -957,7 +958,7 @@ impl ThreadManager {
|
||||
initial_history,
|
||||
session_source: Some(session_source),
|
||||
thread_source,
|
||||
supports_openai_form_elicitation,
|
||||
client_mcp_extensions,
|
||||
..StartThreadOptions::new(config)
|
||||
};
|
||||
let mut request = ThreadSpawnRequest::new(options, auth_manager, agent_control);
|
||||
@@ -1046,7 +1047,7 @@ impl ThreadManager {
|
||||
history,
|
||||
thread_source,
|
||||
parent_trace,
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
ClientMcpExtensions::default(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1077,7 +1078,7 @@ impl ThreadManager {
|
||||
history: InitialHistory,
|
||||
thread_source: Option<ThreadSource>,
|
||||
parent_trace: Option<W3cTraceContext>,
|
||||
supports_openai_form_elicitation: bool,
|
||||
client_mcp_extensions: ClientMcpExtensions,
|
||||
) -> CodexResult<NewThread>
|
||||
where
|
||||
S: Into<ForkSnapshot>,
|
||||
@@ -1091,7 +1092,7 @@ impl ThreadManager {
|
||||
},
|
||||
thread_source,
|
||||
parent_trace,
|
||||
supports_openai_form_elicitation,
|
||||
client_mcp_extensions,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1103,7 +1104,7 @@ impl ThreadManager {
|
||||
prepared: PreparedFork,
|
||||
thread_source: Option<ThreadSource>,
|
||||
parent_trace: Option<W3cTraceContext>,
|
||||
supports_openai_form_elicitation: bool,
|
||||
client_mcp_extensions: ClientMcpExtensions,
|
||||
) -> CodexResult<NewThread> {
|
||||
let history = InitialHistory::Resumed(ResumedHistory {
|
||||
conversation_id: prepared.source_thread_id,
|
||||
@@ -1124,7 +1125,7 @@ impl ThreadManager {
|
||||
},
|
||||
thread_source,
|
||||
parent_trace,
|
||||
supports_openai_form_elicitation,
|
||||
client_mcp_extensions,
|
||||
)
|
||||
.await;
|
||||
drop(prepared);
|
||||
@@ -1137,7 +1138,7 @@ impl ThreadManager {
|
||||
fork_history: ForkHistory,
|
||||
thread_source: Option<ThreadSource>,
|
||||
parent_trace: Option<W3cTraceContext>,
|
||||
supports_openai_form_elicitation: bool,
|
||||
client_mcp_extensions: ClientMcpExtensions,
|
||||
) -> CodexResult<NewThread> {
|
||||
let ForkHistory {
|
||||
snapshot,
|
||||
@@ -1169,7 +1170,7 @@ impl ThreadManager {
|
||||
initial_history: history,
|
||||
thread_source,
|
||||
parent_trace,
|
||||
supports_openai_form_elicitation,
|
||||
client_mcp_extensions,
|
||||
..StartThreadOptions::new(config)
|
||||
};
|
||||
let mut request =
|
||||
@@ -1504,12 +1505,14 @@ impl ThreadManagerState {
|
||||
inherited_exec_policy: Option<Arc<crate::exec_policy::ExecPolicyManager>>,
|
||||
environments: Option<Vec<TurnEnvironmentSelection>>,
|
||||
) -> CodexResult<NewThread> {
|
||||
let client_mcp_extensions = self.client_mcp_extensions_for_child(parent_thread_id).await;
|
||||
let options = StartThreadOptions {
|
||||
history_mode,
|
||||
session_source: Some(session_source),
|
||||
thread_source,
|
||||
metrics_service_name,
|
||||
environments,
|
||||
client_mcp_extensions,
|
||||
..StartThreadOptions::new(config)
|
||||
};
|
||||
let mut request =
|
||||
@@ -1534,11 +1537,13 @@ impl ThreadManagerState {
|
||||
inherited_environments,
|
||||
inherited_exec_policy,
|
||||
} = options;
|
||||
let client_mcp_extensions = self.client_mcp_extensions_for_child(parent_thread_id).await;
|
||||
let thread_source = initial_history.get_resumed_thread_source();
|
||||
let options = StartThreadOptions {
|
||||
initial_history,
|
||||
session_source: Some(session_source),
|
||||
thread_source,
|
||||
client_mcp_extensions,
|
||||
..StartThreadOptions::new(config)
|
||||
};
|
||||
let mut request =
|
||||
@@ -1565,6 +1570,7 @@ impl ThreadManagerState {
|
||||
environments: Option<Vec<TurnEnvironmentSelection>>,
|
||||
thread_extension_init: ExtensionDataInit,
|
||||
) -> CodexResult<NewThread> {
|
||||
let client_mcp_extensions = self.client_mcp_extensions_for_child(parent_thread_id).await;
|
||||
let options = StartThreadOptions {
|
||||
initial_history,
|
||||
history_mode,
|
||||
@@ -1572,6 +1578,7 @@ impl ThreadManagerState {
|
||||
thread_source,
|
||||
environments,
|
||||
thread_extension_init,
|
||||
client_mcp_extensions,
|
||||
..StartThreadOptions::new(config)
|
||||
};
|
||||
let mut request =
|
||||
@@ -1583,6 +1590,19 @@ impl ThreadManagerState {
|
||||
Box::pin(self.spawn_thread(request)).await
|
||||
}
|
||||
|
||||
async fn client_mcp_extensions_for_child(
|
||||
&self,
|
||||
parent_thread_id: Option<ThreadId>,
|
||||
) -> ClientMcpExtensions {
|
||||
let Some(parent_thread_id) = parent_thread_id else {
|
||||
return ClientMcpExtensions::default();
|
||||
};
|
||||
self.get_thread(parent_thread_id)
|
||||
.await
|
||||
.map(|parent| parent.session.services.client_mcp_extensions.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Spawn a new thread with optional history and register it with the manager.
|
||||
async fn spawn_thread(&self, request: ThreadSpawnRequest) -> CodexResult<NewThread> {
|
||||
let ThreadSpawnRequest {
|
||||
@@ -1608,7 +1628,7 @@ impl ThreadManagerState {
|
||||
parent_trace,
|
||||
environments,
|
||||
thread_extension_init,
|
||||
supports_openai_form_elicitation,
|
||||
client_mcp_extensions,
|
||||
} = options;
|
||||
let session_source = session_source.unwrap_or_else(|| self.session_source.clone());
|
||||
let environments = environments.unwrap_or_else(|| {
|
||||
@@ -1704,7 +1724,7 @@ impl ThreadManagerState {
|
||||
parent_trace,
|
||||
environment_selections: environments,
|
||||
thread_extension_init,
|
||||
supports_openai_form_elicitation,
|
||||
client_mcp_extensions,
|
||||
analytics_events_client: self.analytics_events_client.clone(),
|
||||
thread_store: Arc::clone(&self.thread_store),
|
||||
attestation_provider: self.attestation_provider.clone(),
|
||||
|
||||
@@ -13,6 +13,9 @@ use codex_models_manager::manager::RefreshStrategy;
|
||||
use codex_protocol::ResponseItemId;
|
||||
use codex_protocol::capabilities::CapabilityRootLocation;
|
||||
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
||||
use codex_protocol::mcp::ClientMcpExtensions;
|
||||
use codex_protocol::mcp::MCP_APP_UI_EXTENSION_ID;
|
||||
use codex_protocol::mcp::OPENAI_FORM_EXTENSION_ID;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ReasoningItemReasoningSummary;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
@@ -39,6 +42,52 @@ use wiremock::MockServer;
|
||||
|
||||
const TEST_INSTALLATION_ID: &str = "11111111-1111-4111-8111-111111111111";
|
||||
|
||||
#[tokio::test]
|
||||
async fn child_session_inherits_client_mcp_extensions() {
|
||||
let temp_dir = tempdir().expect("tempdir");
|
||||
let mut config = test_config().await;
|
||||
config.codex_home = temp_dir.path().join("codex-home").abs();
|
||||
config.cwd = config.codex_home.abs();
|
||||
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
|
||||
let manager = ThreadManager::with_models_provider_and_home_for_tests(
|
||||
CodexAuth::from_api_key("dummy"),
|
||||
config.model_provider.clone(),
|
||||
config.codex_home.to_path_buf(),
|
||||
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
);
|
||||
let parent = manager
|
||||
.start_thread(StartThreadOptions {
|
||||
client_mcp_extensions: ClientMcpExtensions::new(HashMap::from([
|
||||
(OPENAI_FORM_EXTENSION_ID.to_string(), serde_json::json!({})),
|
||||
(
|
||||
MCP_APP_UI_EXTENSION_ID.to_string(),
|
||||
serde_json::json!({
|
||||
"mimeTypes": ["text/html;profile=mcp-app"],
|
||||
}),
|
||||
),
|
||||
])),
|
||||
..StartThreadOptions::new(config)
|
||||
})
|
||||
.await
|
||||
.expect("start parent thread");
|
||||
|
||||
assert_eq!(
|
||||
manager
|
||||
.state
|
||||
.client_mcp_extensions_for_child(Some(parent.thread_id))
|
||||
.await,
|
||||
ClientMcpExtensions::new(HashMap::from([
|
||||
(OPENAI_FORM_EXTENSION_ID.to_string(), serde_json::json!({})),
|
||||
(
|
||||
MCP_APP_UI_EXTENSION_ID.to_string(),
|
||||
serde_json::json!({
|
||||
"mimeTypes": ["text/html;profile=mcp-app"],
|
||||
}),
|
||||
),
|
||||
]))
|
||||
);
|
||||
}
|
||||
|
||||
struct FakeAgentGraphStore {
|
||||
root_thread_id: ThreadId,
|
||||
descendant_thread_ids: Vec<ThreadId>,
|
||||
@@ -960,7 +1009,7 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
|
||||
rollout_path.clone(),
|
||||
auth_manager,
|
||||
/*parent_trace*/ None,
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
ClientMcpExtensions::default(),
|
||||
)
|
||||
.await
|
||||
.expect("resume source thread");
|
||||
@@ -1117,7 +1166,7 @@ async fn resume_active_thread_from_rollout_returns_running_thread() {
|
||||
rollout_path,
|
||||
auth_manager,
|
||||
/*parent_trace*/ None,
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
ClientMcpExtensions::default(),
|
||||
)
|
||||
.await
|
||||
.expect("resume active source thread");
|
||||
@@ -1184,7 +1233,7 @@ async fn resume_stopped_thread_from_rollout_spawns_new_thread() {
|
||||
rollout_path,
|
||||
auth_manager,
|
||||
/*parent_trace*/ None,
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
ClientMcpExtensions::default(),
|
||||
)
|
||||
.await
|
||||
.expect("resume stopped source thread");
|
||||
@@ -1258,7 +1307,7 @@ async fn resume_stopped_thread_from_rollout_preserves_thread_source() {
|
||||
rollout_path,
|
||||
auth_manager,
|
||||
/*parent_trace*/ None,
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
ClientMcpExtensions::default(),
|
||||
)
|
||||
.await
|
||||
.expect("resume source thread");
|
||||
@@ -1384,7 +1433,7 @@ async fn rollout_path_resume_and_fork_read_history_through_thread_store() {
|
||||
}),
|
||||
auth_manager.clone(),
|
||||
/*parent_trace*/ None,
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
ClientMcpExtensions::default(),
|
||||
)
|
||||
.await
|
||||
.expect("seed rollout path in store");
|
||||
@@ -1401,7 +1450,7 @@ async fn rollout_path_resume_and_fork_read_history_through_thread_store() {
|
||||
rollout_path.clone(),
|
||||
auth_manager,
|
||||
/*parent_trace*/ None,
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
ClientMcpExtensions::default(),
|
||||
)
|
||||
.await
|
||||
.expect("resume from rollout path");
|
||||
@@ -1772,7 +1821,7 @@ async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_histor
|
||||
]),
|
||||
auth_manager,
|
||||
/*parent_trace*/ None,
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
ClientMcpExtensions::default(),
|
||||
)
|
||||
.await
|
||||
.expect("create source thread from completed history");
|
||||
@@ -1892,7 +1941,7 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() {
|
||||
]),
|
||||
auth_manager,
|
||||
/*parent_trace*/ None,
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
ClientMcpExtensions::default(),
|
||||
)
|
||||
.await
|
||||
.expect("create source thread from explicit partial history");
|
||||
@@ -1988,7 +2037,7 @@ async fn interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_
|
||||
]),
|
||||
auth_manager,
|
||||
/*parent_trace*/ None,
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
ClientMcpExtensions::default(),
|
||||
)
|
||||
.await
|
||||
.expect("create source thread from partial history");
|
||||
|
||||
@@ -30,6 +30,7 @@ use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicy;
|
||||
use codex_protocol::mcp::ClientMcpExtensions;
|
||||
use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::FunctionCallOutputBody;
|
||||
@@ -2730,7 +2731,7 @@ async fn resume_agent_restores_closed_agent_and_accepts_send_input() {
|
||||
})]),
|
||||
AuthManager::from_auth_for_testing(CodexAuth::from_api_key("dummy")),
|
||||
/*parent_trace*/ None,
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
ClientMcpExtensions::default(),
|
||||
)
|
||||
.await
|
||||
.expect("start thread");
|
||||
|
||||
Reference in New Issue
Block a user