mirror of
https://github.com/openai/codex.git
synced 2026-05-21 19:45:26 +00:00
Compare commits
8 Commits
pr23165
...
etraut/xco
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdbd0629ea | ||
|
|
41f4b83259 | ||
|
|
6a37b1d53e | ||
|
|
725bfb2514 | ||
|
|
1113126c80 | ||
|
|
efc64b0349 | ||
|
|
58fdaec898 | ||
|
|
5db02d3bb5 |
@@ -59,6 +59,8 @@ use codex_app_server_protocol::ServerRequestPayload;
|
||||
use codex_app_server_protocol::experimental_required_message;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_chatgpt::workspace_settings;
|
||||
use codex_core::ClientCompatibilityFlags;
|
||||
use codex_core::McpElicitationCompatibility;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::thread_store_from_config;
|
||||
@@ -187,6 +189,26 @@ pub(crate) struct InitializedConnectionSessionState {
|
||||
pub(crate) opted_out_notification_methods: HashSet<String>,
|
||||
pub(crate) app_server_client_name: String,
|
||||
pub(crate) client_version: String,
|
||||
pub(crate) client_compatibility_flags: ClientCompatibilityFlags,
|
||||
}
|
||||
|
||||
fn client_compatibility_flags_for_app_server_client(
|
||||
client_name: &str,
|
||||
client_version: &str,
|
||||
) -> ClientCompatibilityFlags {
|
||||
// Compatibility hack for Xcode 26.4: that client shipped against the
|
||||
// pre-PR #17043 MCP surface, where only `codex_apps` advertised elicitation
|
||||
// support. Advertising custom-server elicitations creates app-server
|
||||
// events/messages that Xcode 26.4 cannot handle, so keep that client on the
|
||||
// old capability surface.
|
||||
// TODO: Remove once Xcode 26.4 ages out of support.
|
||||
let mcp_elicitation =
|
||||
if client_name.eq_ignore_ascii_case("xcode") && client_version.starts_with("26.4") {
|
||||
McpElicitationCompatibility::CodexAppsOnly
|
||||
} else {
|
||||
McpElicitationCompatibility::Default
|
||||
};
|
||||
ClientCompatibilityFlags { mcp_elicitation }
|
||||
}
|
||||
|
||||
impl Default for ConnectionSessionState {
|
||||
@@ -195,6 +217,27 @@ impl Default for ConnectionSessionState {
|
||||
}
|
||||
}
|
||||
|
||||
impl InitializedConnectionSessionState {
|
||||
pub(crate) fn new(
|
||||
experimental_api_enabled: bool,
|
||||
opted_out_notification_methods: HashSet<String>,
|
||||
app_server_client_name: String,
|
||||
client_version: String,
|
||||
) -> Self {
|
||||
let client_compatibility_flags = client_compatibility_flags_for_app_server_client(
|
||||
&app_server_client_name,
|
||||
&client_version,
|
||||
);
|
||||
Self {
|
||||
experimental_api_enabled,
|
||||
opted_out_notification_methods,
|
||||
app_server_client_name,
|
||||
client_version,
|
||||
client_compatibility_flags,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectionSessionState {
|
||||
pub(crate) fn new(origin: ConnectionOrigin) -> Self {
|
||||
Self {
|
||||
@@ -237,6 +280,13 @@ impl ConnectionSessionState {
|
||||
.map(|session| session.client_version.as_str())
|
||||
}
|
||||
|
||||
pub(crate) fn client_compatibility_flags(&self) -> ClientCompatibilityFlags {
|
||||
self.initialized
|
||||
.get()
|
||||
.map(|session| session.client_compatibility_flags)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn initialize(&self, session: InitializedConnectionSessionState) -> Result<(), ()> {
|
||||
self.initialized.set(session).map_err(|_| ())
|
||||
}
|
||||
@@ -756,11 +806,10 @@ impl MessageProcessor {
|
||||
);
|
||||
|
||||
let serialization_scope = codex_request.serialization_scope();
|
||||
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 device_key_requests_allowed = session.allows_device_key_requests();
|
||||
let error_request_id = connection_request_id.clone();
|
||||
let rpc_gate = Arc::clone(&session.rpc_gate);
|
||||
let client_session = Arc::clone(&session);
|
||||
let processor = Arc::clone(self);
|
||||
let span = request_context.span();
|
||||
let request = QueuedInitializedRequest::new(
|
||||
@@ -772,8 +821,7 @@ impl MessageProcessor {
|
||||
connection_request_id,
|
||||
codex_request,
|
||||
request_context,
|
||||
app_server_client_name,
|
||||
client_version,
|
||||
client_session,
|
||||
device_key_requests_allowed,
|
||||
)
|
||||
.await;
|
||||
@@ -802,8 +850,7 @@ impl MessageProcessor {
|
||||
connection_request_id: ConnectionRequestId,
|
||||
codex_request: ClientRequest,
|
||||
request_context: RequestContext,
|
||||
app_server_client_name: Option<String>,
|
||||
client_version: Option<String>,
|
||||
client_session: Arc<ConnectionSessionState>,
|
||||
device_key_requests_allowed: bool,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let connection_id = connection_request_id.connection_id;
|
||||
@@ -811,7 +858,6 @@ impl MessageProcessor {
|
||||
connection_id,
|
||||
request_id: codex_request.id().clone(),
|
||||
};
|
||||
|
||||
let result: Result<Option<ClientResponsePayload>, JSONRPCErrorError> = match codex_request {
|
||||
ClientRequest::Initialize { .. } => {
|
||||
panic!("Initialize should be handled before initialized request dispatch");
|
||||
@@ -926,8 +972,7 @@ impl MessageProcessor {
|
||||
.thread_start(
|
||||
request_id.clone(),
|
||||
params,
|
||||
app_server_client_name.clone(),
|
||||
client_version.clone(),
|
||||
client_session.as_ref(),
|
||||
request_context,
|
||||
)
|
||||
.await
|
||||
@@ -939,12 +984,12 @@ impl MessageProcessor {
|
||||
}
|
||||
ClientRequest::ThreadResume { params, .. } => {
|
||||
self.thread_processor
|
||||
.thread_resume(request_id.clone(), params)
|
||||
.thread_resume(request_id.clone(), params, client_session.as_ref())
|
||||
.await
|
||||
}
|
||||
ClientRequest::ThreadFork { params, .. } => {
|
||||
self.thread_processor
|
||||
.thread_fork(request_id.clone(), params)
|
||||
.thread_fork(request_id.clone(), params, client_session.as_ref())
|
||||
.await
|
||||
}
|
||||
ClientRequest::ThreadArchive { params, .. } => {
|
||||
@@ -1095,12 +1140,7 @@ impl MessageProcessor {
|
||||
}
|
||||
ClientRequest::TurnStart { params, .. } => {
|
||||
self.turn_processor
|
||||
.turn_start(
|
||||
request_id.clone(),
|
||||
params,
|
||||
app_server_client_name.clone(),
|
||||
client_version.clone(),
|
||||
)
|
||||
.turn_start(request_id.clone(), params, client_session.as_ref())
|
||||
.await
|
||||
}
|
||||
ClientRequest::ThreadInjectItems { params, .. } => {
|
||||
@@ -1138,7 +1178,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, client_session.as_ref())
|
||||
.await
|
||||
}
|
||||
ClientRequest::McpServerOauthLogin { params, .. } => {
|
||||
self.mcp_processor.mcp_server_oauth_login(params).await
|
||||
@@ -1148,12 +1190,12 @@ impl MessageProcessor {
|
||||
}
|
||||
ClientRequest::McpServerStatusList { params, .. } => {
|
||||
self.mcp_processor
|
||||
.mcp_server_status_list(&request_id, params)
|
||||
.mcp_server_status_list(&request_id, params, client_session.as_ref())
|
||||
.await
|
||||
}
|
||||
ClientRequest::McpResourceRead { params, .. } => {
|
||||
self.mcp_processor
|
||||
.mcp_resource_read(&request_id, params)
|
||||
.mcp_resource_read(&request_id, params, client_session.as_ref())
|
||||
.await
|
||||
}
|
||||
ClientRequest::McpServerToolCall { params, .. } => {
|
||||
@@ -1259,3 +1301,29 @@ impl MessageProcessor {
|
||||
#[cfg(test)]
|
||||
#[path = "message_processor_tracing_tests.rs"]
|
||||
mod message_processor_tracing_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod client_compatibility_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn xcode_26_4_uses_legacy_mcp_elicitation_compatibility() {
|
||||
use McpElicitationCompatibility::CodexAppsOnly;
|
||||
use McpElicitationCompatibility::Default as DefaultCompatibility;
|
||||
|
||||
let cases = [
|
||||
("Xcode", "26.4", CodexAppsOnly),
|
||||
("xcode", "26.4.1", CodexAppsOnly),
|
||||
("Xcode", "26.4-beta", CodexAppsOnly),
|
||||
("Xcode", "26.3", DefaultCompatibility),
|
||||
("Xcode", "26.5", DefaultCompatibility),
|
||||
("codex-vscode", "26.4", DefaultCompatibility),
|
||||
];
|
||||
for (name, version, expected) in cases {
|
||||
assert_eq!(
|
||||
client_compatibility_flags_for_app_server_client(name, version).mcp_elicitation,
|
||||
expected
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,6 +244,7 @@ use codex_config::CloudRequirementsLoadErrorCode;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::loader::project_trust_key;
|
||||
use codex_config::types::McpServerTransportConfig;
|
||||
use codex_core::ClientCompatibilityFlags;
|
||||
use codex_core::CodexThread;
|
||||
use codex_core::CodexThreadTurnContextOverrides;
|
||||
use codex_core::ForkSnapshot;
|
||||
@@ -460,6 +461,7 @@ use crate::error_code::internal_error;
|
||||
use crate::error_code::invalid_request;
|
||||
use crate::filters::compute_source_filters;
|
||||
use crate::filters::source_kind_matches;
|
||||
use crate::message_processor::ConnectionSessionState;
|
||||
use crate::thread_state::ThreadListenerCommand;
|
||||
use crate::thread_state::ThreadState;
|
||||
use crate::thread_state::ThreadStateManager;
|
||||
@@ -482,6 +484,10 @@ pub(crate) use self::thread_summary::read_rollout_items_from_rollout;
|
||||
pub(crate) use self::thread_summary::read_summary_from_rollout;
|
||||
pub(crate) use self::thread_summary::summary_to_thread;
|
||||
|
||||
fn apply_client_compatibility(config: &mut Config, session: &ConnectionSessionState) {
|
||||
config.client_compatibility_flags = session.client_compatibility_flags();
|
||||
}
|
||||
|
||||
pub(crate) fn build_api_turns_from_rollout_items(items: &[RolloutItem]) -> Vec<Turn> {
|
||||
let mut builder = ThreadHistoryBuilder::new();
|
||||
for item in items {
|
||||
|
||||
@@ -90,12 +90,12 @@ impl InitializeRequestProcessor {
|
||||
let user_agent_suffix = format!("{name}; {version}");
|
||||
let codex_home = self.config.codex_home.clone();
|
||||
if session
|
||||
.initialize(InitializedConnectionSessionState {
|
||||
.initialize(InitializedConnectionSessionState::new(
|
||||
experimental_api_enabled,
|
||||
opted_out_notification_methods: opt_out_notification_methods.into_iter().collect(),
|
||||
app_server_client_name: name.clone(),
|
||||
client_version: version,
|
||||
})
|
||||
opt_out_notification_methods.into_iter().collect(),
|
||||
name.clone(),
|
||||
version,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
return Err(invalid_request("Already initialized"));
|
||||
|
||||
@@ -47,8 +47,9 @@ impl McpRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ListMcpServerStatusParams,
|
||||
client_session: &ConnectionSessionState,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.list_mcp_server_status(request_id, params)
|
||||
self.list_mcp_server_status(request_id, params, client_session)
|
||||
.await
|
||||
.map(|()| None)
|
||||
}
|
||||
@@ -57,8 +58,9 @@ impl McpRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: McpResourceReadParams,
|
||||
client_session: &ConnectionSessionState,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.read_mcp_resource(request_id, params)
|
||||
self.read_mcp_resource(request_id, params, client_session)
|
||||
.await
|
||||
.map(|()| None)
|
||||
}
|
||||
@@ -248,8 +250,10 @@ impl McpRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ListMcpServerStatusParams,
|
||||
client_session: &ConnectionSessionState,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let request = request_id.clone();
|
||||
let client_compatibility_flags = client_session.client_compatibility_flags();
|
||||
|
||||
let outgoing = Arc::clone(&self.outgoing);
|
||||
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
|
||||
@@ -271,41 +275,21 @@ impl McpRequestProcessor {
|
||||
};
|
||||
|
||||
tokio::spawn(async move {
|
||||
Self::list_mcp_server_status_task(
|
||||
outgoing,
|
||||
request,
|
||||
let result = Self::list_mcp_server_status_response(
|
||||
request.request_id.to_string(),
|
||||
params,
|
||||
config,
|
||||
mcp_config,
|
||||
auth,
|
||||
runtime_environment,
|
||||
client_compatibility_flags,
|
||||
)
|
||||
.await;
|
||||
outgoing.send_result(request, result).await;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_mcp_server_status_task(
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ListMcpServerStatusParams,
|
||||
config: Config,
|
||||
mcp_config: codex_mcp::McpConfig,
|
||||
auth: Option<CodexAuth>,
|
||||
runtime_environment: McpRuntimeEnvironment,
|
||||
) {
|
||||
let result = Self::list_mcp_server_status_response(
|
||||
request_id.request_id.to_string(),
|
||||
params,
|
||||
config,
|
||||
mcp_config,
|
||||
auth,
|
||||
runtime_environment,
|
||||
)
|
||||
.await;
|
||||
outgoing.send_result(request_id, result).await;
|
||||
}
|
||||
|
||||
async fn list_mcp_server_status_response(
|
||||
request_id: String,
|
||||
params: ListMcpServerStatusParams,
|
||||
@@ -313,6 +297,7 @@ impl McpRequestProcessor {
|
||||
mcp_config: codex_mcp::McpConfig,
|
||||
auth: Option<CodexAuth>,
|
||||
runtime_environment: McpRuntimeEnvironment,
|
||||
client_compatibility_flags: ClientCompatibilityFlags,
|
||||
) -> Result<ListMcpServerStatusResponse, JSONRPCErrorError> {
|
||||
let detail = match params.detail.unwrap_or(McpServerStatusDetail::Full) {
|
||||
McpServerStatusDetail::Full => McpSnapshotDetail::Full,
|
||||
@@ -325,6 +310,7 @@ impl McpRequestProcessor {
|
||||
request_id,
|
||||
runtime_environment,
|
||||
detail,
|
||||
client_compatibility_flags.mcp_elicitation,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -398,8 +384,10 @@ impl McpRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: McpResourceReadParams,
|
||||
client_session: &ConnectionSessionState,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let outgoing = Arc::clone(&self.outgoing);
|
||||
let client_compatibility_flags = client_session.client_compatibility_flags();
|
||||
let McpResourceReadParams {
|
||||
thread_id,
|
||||
server,
|
||||
@@ -440,6 +428,7 @@ impl McpRequestProcessor {
|
||||
runtime_environment,
|
||||
&server,
|
||||
&uri,
|
||||
client_compatibility_flags.mcp_elicitation,
|
||||
)
|
||||
.await
|
||||
.and_then(|result| serde_json::to_value(result).map_err(anyhow::Error::from));
|
||||
|
||||
@@ -301,19 +301,12 @@ impl ThreadRequestProcessor {
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadStartParams,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
client_session: &ConnectionSessionState,
|
||||
request_context: RequestContext,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.thread_start_inner(
|
||||
request_id,
|
||||
params,
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
request_context,
|
||||
)
|
||||
.await
|
||||
.map(|()| None)
|
||||
self.thread_start_inner(request_id, params, client_session, request_context)
|
||||
.await
|
||||
.map(|()| None)
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_unsubscribe(
|
||||
@@ -330,8 +323,9 @@ impl ThreadRequestProcessor {
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadResumeParams,
|
||||
client_session: &ConnectionSessionState,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.thread_resume_inner(request_id, params)
|
||||
self.thread_resume_inner(request_id, params, client_session)
|
||||
.await
|
||||
.map(|()| None)
|
||||
}
|
||||
@@ -340,8 +334,9 @@ impl ThreadRequestProcessor {
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadForkParams,
|
||||
client_session: &ConnectionSessionState,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.thread_fork_inner(request_id, params)
|
||||
self.thread_fork_inner(request_id, params, client_session)
|
||||
.await
|
||||
.map(|()| None)
|
||||
}
|
||||
@@ -582,16 +577,6 @@ impl ThreadRequestProcessor {
|
||||
|
||||
Ok((thread_id, thread))
|
||||
}
|
||||
async fn acquire_thread_list_state_permit(
|
||||
&self,
|
||||
) -> Result<SemaphorePermit<'_>, JSONRPCErrorError> {
|
||||
self.thread_list_state_permit
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
internal_error(format!("failed to acquire thread list state permit: {err}"))
|
||||
})
|
||||
}
|
||||
|
||||
async fn set_app_server_client_info(
|
||||
thread: &CodexThread,
|
||||
@@ -608,6 +593,17 @@ impl ThreadRequestProcessor {
|
||||
})
|
||||
}
|
||||
|
||||
async fn acquire_thread_list_state_permit(
|
||||
&self,
|
||||
) -> Result<SemaphorePermit<'_>, JSONRPCErrorError> {
|
||||
self.thread_list_state_permit
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
internal_error(format!("failed to acquire thread list state permit: {err}"))
|
||||
})
|
||||
}
|
||||
|
||||
async fn finalize_thread_teardown(&self, thread_id: ThreadId) {
|
||||
self.pending_thread_unloads.lock().await.remove(&thread_id);
|
||||
self.outgoing
|
||||
@@ -716,8 +712,7 @@ impl ThreadRequestProcessor {
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadStartParams,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
client_session: &ConnectionSessionState,
|
||||
request_context: RequestContext,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let ThreadStartParams {
|
||||
@@ -777,6 +772,9 @@ impl ThreadRequestProcessor {
|
||||
let config_manager = self.config_manager.clone();
|
||||
let outgoing = Arc::clone(&listener_task_context.outgoing);
|
||||
let error_request_id = request_id.clone();
|
||||
let app_server_client_name = client_session.app_server_client_name().map(str::to_string);
|
||||
let app_server_client_version = client_session.client_version().map(str::to_string);
|
||||
let client_compatibility_flags = client_session.client_compatibility_flags();
|
||||
let thread_start_task = async move {
|
||||
if let Err(error) = Self::thread_start_task(
|
||||
listener_task_context,
|
||||
@@ -784,6 +782,7 @@ impl ThreadRequestProcessor {
|
||||
request_id,
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
client_compatibility_flags,
|
||||
config,
|
||||
typesafe_overrides,
|
||||
dynamic_tools,
|
||||
@@ -856,6 +855,7 @@ impl ThreadRequestProcessor {
|
||||
request_id: ConnectionRequestId,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
client_compatibility_flags: ClientCompatibilityFlags,
|
||||
config_overrides: Option<HashMap<String, serde_json::Value>>,
|
||||
typesafe_overrides: ConfigOverrides,
|
||||
dynamic_tools: Option<Vec<ApiDynamicToolSpec>>,
|
||||
@@ -960,6 +960,7 @@ impl ThreadRequestProcessor {
|
||||
.collect()
|
||||
};
|
||||
let core_dynamic_tool_count = core_dynamic_tools.len();
|
||||
config.client_compatibility_flags = client_compatibility_flags;
|
||||
|
||||
let NewThread {
|
||||
thread_id,
|
||||
@@ -2296,6 +2297,7 @@ impl ThreadRequestProcessor {
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadResumeParams,
|
||||
client_session: &ConnectionSessionState,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
if let Ok(thread_id) = ThreadId::from_string(¶ms.thread_id)
|
||||
&& self
|
||||
@@ -2400,7 +2402,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
|
||||
@@ -2412,6 +2414,7 @@ impl ThreadRequestProcessor {
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
apply_client_compatibility(&mut config, client_session);
|
||||
|
||||
let instruction_sources = Self::instruction_sources_from_config(&config).await;
|
||||
let response_history = thread_history.clone();
|
||||
@@ -2931,6 +2934,7 @@ impl ThreadRequestProcessor {
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadForkParams,
|
||||
client_session: &ConnectionSessionState,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let ThreadForkParams {
|
||||
thread_id,
|
||||
@@ -3009,11 +3013,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))?;
|
||||
apply_client_compatibility(&mut config, client_session);
|
||||
|
||||
let fallback_model_provider = config.model_provider_id.clone();
|
||||
let instruction_sources = Self::instruction_sources_from_config(&config).await;
|
||||
|
||||
@@ -49,17 +49,11 @@ impl TurnRequestProcessor {
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: TurnStartParams,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
client_session: &ConnectionSessionState,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.turn_start_inner(
|
||||
request_id,
|
||||
params,
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
)
|
||||
.await
|
||||
.map(|response| Some(response.into()))
|
||||
self.turn_start_inner(request_id, params, client_session)
|
||||
.await
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_inject_items(
|
||||
@@ -146,8 +140,9 @@ impl TurnRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ReviewStartParams,
|
||||
client_session: &ConnectionSessionState,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.review_start_inner(request_id, params)
|
||||
self.review_start_inner(request_id, params, client_session)
|
||||
.await
|
||||
.map(|()| None)
|
||||
}
|
||||
@@ -330,8 +325,7 @@ impl TurnRequestProcessor {
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: TurnStartParams,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
client_session: &ConnectionSessionState,
|
||||
) -> Result<TurnStartResponse, JSONRPCErrorError> {
|
||||
if let Err(error) = Self::validate_v2_input_limit(¶ms.input) {
|
||||
self.track_error_response(
|
||||
@@ -349,8 +343,8 @@ impl TurnRequestProcessor {
|
||||
})?;
|
||||
Self::set_app_server_client_info(
|
||||
thread.as_ref(),
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
client_session.app_server_client_name().map(str::to_string),
|
||||
client_session.client_version().map(str::to_string),
|
||||
)
|
||||
.await
|
||||
.inspect_err(|error| {
|
||||
@@ -885,6 +879,7 @@ impl TurnRequestProcessor {
|
||||
request_id: &ConnectionRequestId,
|
||||
parent_thread_id: ThreadId,
|
||||
parent_thread: Arc<CodexThread>,
|
||||
client_session: &ConnectionSessionState,
|
||||
review_request: ReviewRequest,
|
||||
display_text: &str,
|
||||
) -> std::result::Result<(), JSONRPCErrorError> {
|
||||
@@ -904,6 +899,7 @@ impl TurnRequestProcessor {
|
||||
};
|
||||
|
||||
let mut config = self.config.as_ref().clone();
|
||||
apply_client_compatibility(&mut config, client_session);
|
||||
if let Some(review_model) = &config.review_model {
|
||||
config.model = Some(review_model.clone());
|
||||
}
|
||||
@@ -996,6 +992,7 @@ impl TurnRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ReviewStartParams,
|
||||
client_session: &ConnectionSessionState,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let ReviewStartParams {
|
||||
thread_id,
|
||||
@@ -1021,6 +1018,7 @@ impl TurnRequestProcessor {
|
||||
request_id,
|
||||
parent_thread_id,
|
||||
parent_thread,
|
||||
client_session,
|
||||
review_request,
|
||||
display_text.as_str(),
|
||||
)
|
||||
|
||||
@@ -142,13 +142,17 @@ impl McpConnectionManager {
|
||||
codex_apps_tools_cache_key: CodexAppsToolsCacheKey,
|
||||
tool_plugin_provenance: ToolPluginProvenance,
|
||||
auth: Option<&CodexAuth>,
|
||||
elicitation_compatibility: crate::elicitation::McpElicitationCompatibility,
|
||||
) -> (Self, CancellationToken) {
|
||||
let cancel_token = CancellationToken::new();
|
||||
let mut clients = HashMap::new();
|
||||
let mut server_origins = HashMap::new();
|
||||
let mut join_set = JoinSet::new();
|
||||
let elicitation_requests =
|
||||
ElicitationRequestManager::new(approval_policy.value(), initial_permission_profile);
|
||||
let elicitation_requests = ElicitationRequestManager::new_with_compatibility(
|
||||
approval_policy.value(),
|
||||
initial_permission_profile,
|
||||
elicitation_compatibility,
|
||||
);
|
||||
let tool_plugin_provenance = Arc::new(tool_plugin_provenance);
|
||||
let startup_submit_id = submit_id.clone();
|
||||
let codex_apps_auth_provider = auth
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::codex_apps::read_cached_codex_apps_tools;
|
||||
use crate::codex_apps::write_cached_codex_apps_tools;
|
||||
use crate::declared_openai_file_input_param_names;
|
||||
use crate::elicitation::ElicitationRequestManager;
|
||||
use crate::elicitation::McpElicitationCompatibility;
|
||||
use crate::elicitation::elicitation_is_rejected_by_policy;
|
||||
use crate::rmcp_client::AsyncManagedClient;
|
||||
use crate::rmcp_client::ManagedClient;
|
||||
@@ -25,7 +26,6 @@ use futures::FutureExt;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rmcp::model::CreateElicitationRequestParams;
|
||||
use rmcp::model::ElicitationAction;
|
||||
use rmcp::model::ElicitationCapability;
|
||||
use rmcp::model::JsonObject;
|
||||
use rmcp::model::Meta;
|
||||
use rmcp::model::NumberOrString;
|
||||
@@ -266,6 +266,34 @@ async fn disabled_permissions_do_not_auto_accept_elicitation_with_requested_fiel
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn codex_apps_only_custom_server_elicitation_is_declined_without_event() {
|
||||
let manager = ElicitationRequestManager::new_with_compatibility(
|
||||
AskForApproval::OnRequest,
|
||||
PermissionProfile::default(),
|
||||
McpElicitationCompatibility::CodexAppsOnly,
|
||||
);
|
||||
let (tx_event, rx_event) = async_channel::bounded(1);
|
||||
let sender = manager.make_sender("custom_mcp".to_string(), tx_event);
|
||||
|
||||
let response = sender(
|
||||
NumberOrString::Number(1),
|
||||
CreateElicitationRequestParams::FormElicitationParams {
|
||||
meta: None,
|
||||
message: "What should I say?".to_string(),
|
||||
requested_schema: rmcp::model::ElicitationSchema::builder()
|
||||
.build()
|
||||
.expect("schema should build"),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("elicitation should auto decline");
|
||||
|
||||
assert_eq!(response.action, ElicitationAction::Decline);
|
||||
assert_eq!(response.content, None);
|
||||
assert!(rx_event.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_qualify_tools_short_non_duplicated_names() {
|
||||
let tools = vec![
|
||||
@@ -802,8 +830,8 @@ async fn list_all_tools_uses_startup_snapshot_when_client_startup_fails() {
|
||||
#[test]
|
||||
fn elicitation_capability_uses_2025_06_18_shape_for_all_servers() {
|
||||
for server_name in [CODEX_APPS_MCP_SERVER_NAME, "custom_mcp"] {
|
||||
let capability = elicitation_capability_for_server(server_name);
|
||||
assert_eq!(capability, Some(ElicitationCapability::default()));
|
||||
let capability =
|
||||
elicitation_capability_for_server(server_name, McpElicitationCompatibility::Default);
|
||||
assert_eq!(
|
||||
serde_json::to_value(capability).expect("serialize elicitation capability"),
|
||||
serde_json::json!({})
|
||||
@@ -811,6 +839,18 @@ fn elicitation_capability_uses_2025_06_18_shape_for_all_servers() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_apps_only_elicitation_capability_suppresses_custom_servers() {
|
||||
let compatibility = McpElicitationCompatibility::CodexAppsOnly;
|
||||
assert_eq!(
|
||||
(
|
||||
elicitation_capability_for_server(CODEX_APPS_MCP_SERVER_NAME, compatibility).is_some(),
|
||||
elicitation_capability_for_server("custom_mcp", compatibility).is_some(),
|
||||
),
|
||||
(true, false)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_init_error_display_prompts_for_github_pat() {
|
||||
let server_name = "github";
|
||||
|
||||
@@ -9,6 +9,7 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex as StdMutex;
|
||||
|
||||
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use crate::mcp::McpPermissionPromptAutoApproveContext;
|
||||
use crate::mcp::mcp_permission_prompt_is_auto_approved;
|
||||
use anyhow::Context;
|
||||
@@ -36,17 +37,31 @@ pub(crate) struct ElicitationRequestManager {
|
||||
requests: Arc<Mutex<ResponderMap>>,
|
||||
pub(crate) approval_policy: Arc<StdMutex<AskForApproval>>,
|
||||
pub(crate) permission_profile: Arc<StdMutex<PermissionProfile>>,
|
||||
pub(crate) compatibility: McpElicitationCompatibility,
|
||||
}
|
||||
|
||||
impl ElicitationRequestManager {
|
||||
pub(crate) fn new(
|
||||
approval_policy: AskForApproval,
|
||||
permission_profile: PermissionProfile,
|
||||
) -> Self {
|
||||
Self::new_with_compatibility(
|
||||
approval_policy,
|
||||
permission_profile,
|
||||
McpElicitationCompatibility::Default,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn new_with_compatibility(
|
||||
approval_policy: AskForApproval,
|
||||
permission_profile: PermissionProfile,
|
||||
compatibility: McpElicitationCompatibility,
|
||||
) -> Self {
|
||||
Self {
|
||||
requests: Arc::new(Mutex::new(HashMap::new())),
|
||||
approval_policy: Arc::new(StdMutex::new(approval_policy)),
|
||||
permission_profile: Arc::new(StdMutex::new(permission_profile)),
|
||||
compatibility,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +88,7 @@ impl ElicitationRequestManager {
|
||||
let elicitation_requests = self.requests.clone();
|
||||
let approval_policy = self.approval_policy.clone();
|
||||
let permission_profile = self.permission_profile.clone();
|
||||
let compatibility = self.compatibility;
|
||||
Box::new(move |id, elicitation| {
|
||||
let elicitation_requests = elicitation_requests.clone();
|
||||
let tx_event = tx_event.clone();
|
||||
@@ -80,6 +96,14 @@ impl ElicitationRequestManager {
|
||||
let approval_policy = approval_policy.clone();
|
||||
let permission_profile = permission_profile.clone();
|
||||
async move {
|
||||
if !compatibility.allows_server_elicitation(&server_name) {
|
||||
return Ok(ElicitationResponse {
|
||||
action: ElicitationAction::Decline,
|
||||
content: None,
|
||||
meta: None,
|
||||
});
|
||||
}
|
||||
|
||||
let approval_policy = approval_policy
|
||||
.lock()
|
||||
.map(|policy| *policy)
|
||||
@@ -169,6 +193,22 @@ impl ElicitationRequestManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Compatibility policy for the MCP elicitation capability surface.
|
||||
/// `CodexAppsOnly` preserves the pre-PR #17043 behavior for old clients by
|
||||
/// keeping support for `codex_apps` while suppressing custom MCP servers.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub enum McpElicitationCompatibility {
|
||||
#[default]
|
||||
Default,
|
||||
CodexAppsOnly,
|
||||
}
|
||||
|
||||
impl McpElicitationCompatibility {
|
||||
pub fn allows_server_elicitation(self, server_name: &str) -> bool {
|
||||
self == Self::Default || server_name == CODEX_APPS_MCP_SERVER_NAME
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn elicitation_is_rejected_by_policy(approval_policy: AskForApproval) -> bool {
|
||||
match approval_policy {
|
||||
AskForApproval::Never => true,
|
||||
|
||||
@@ -34,6 +34,7 @@ pub use mcp::resolve_oauth_scopes;
|
||||
pub use mcp::should_retry_without_scopes;
|
||||
|
||||
pub use codex_apps::filter_non_codex_apps_mcp_tools_only;
|
||||
pub use elicitation::McpElicitationCompatibility;
|
||||
pub use mcp::McpPermissionPromptAutoApproveContext;
|
||||
pub use mcp::mcp_permission_prompt_is_auto_approved;
|
||||
pub use mcp::qualified_mcp_tool_name_prefix;
|
||||
|
||||
@@ -231,6 +231,7 @@ pub async fn read_mcp_resource(
|
||||
runtime_environment: McpRuntimeEnvironment,
|
||||
server: &str,
|
||||
uri: &str,
|
||||
elicitation_compatibility: crate::McpElicitationCompatibility,
|
||||
) -> anyhow::Result<ReadResourceResult> {
|
||||
let mut mcp_servers = effective_mcp_servers(config, auth);
|
||||
mcp_servers.retain(|name, _| name == server);
|
||||
@@ -255,6 +256,7 @@ pub async fn read_mcp_resource(
|
||||
codex_apps_tools_cache_key(auth),
|
||||
tool_plugin_provenance(config),
|
||||
auth,
|
||||
elicitation_compatibility,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -285,6 +287,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
|
||||
submit_id: String,
|
||||
runtime_environment: McpRuntimeEnvironment,
|
||||
detail: McpSnapshotDetail,
|
||||
elicitation_compatibility: crate::McpElicitationCompatibility,
|
||||
) -> McpServerStatusSnapshot {
|
||||
let mcp_servers = effective_mcp_servers(config, auth);
|
||||
let tool_plugin_provenance = tool_plugin_provenance(config);
|
||||
@@ -320,6 +323,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
|
||||
codex_apps_tools_cache_key(auth),
|
||||
tool_plugin_provenance,
|
||||
auth,
|
||||
elicitation_compatibility,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ use crate::codex_apps::normalize_codex_apps_callable_namespace;
|
||||
use crate::codex_apps::normalize_codex_apps_tool_title;
|
||||
use crate::codex_apps::write_cached_codex_apps_tools_if_needed;
|
||||
use crate::elicitation::ElicitationRequestManager;
|
||||
use crate::elicitation::McpElicitationCompatibility;
|
||||
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use crate::mcp::ToolPluginProvenance;
|
||||
use crate::runtime::McpRuntimeEnvironment;
|
||||
@@ -318,8 +319,16 @@ impl From<anyhow::Error> for StartupOutcomeError {
|
||||
}
|
||||
|
||||
pub(crate) fn elicitation_capability_for_server(
|
||||
_server_name: &str,
|
||||
server_name: &str,
|
||||
compatibility: McpElicitationCompatibility,
|
||||
) -> Option<ElicitationCapability> {
|
||||
// Keep capability advertising aligned with the client compatibility flags.
|
||||
// The sender has a matching defensive decline path for servers that send
|
||||
// elicitations despite this advertised capability.
|
||||
if !compatibility.allows_server_elicitation(server_name) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation#capabilities
|
||||
// indicates this should be an empty object.
|
||||
Some(ElicitationCapability::default())
|
||||
@@ -457,7 +466,8 @@ async fn start_server_task(
|
||||
elicitation_requests,
|
||||
codex_apps_tools_cache_context,
|
||||
} = params;
|
||||
let elicitation = elicitation_capability_for_server(&server_name);
|
||||
let elicitation =
|
||||
elicitation_capability_for_server(&server_name, elicitation_requests.compatibility);
|
||||
let params = InitializeRequestParams {
|
||||
meta: None,
|
||||
capabilities: ClientCapabilities {
|
||||
|
||||
14
codex-rs/core/src/client_compatibility.rs
Normal file
14
codex-rs/core/src/client_compatibility.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
//! Internal compatibility flags selected by app-server clients.
|
||||
//!
|
||||
//! These flags keep app-server client identity at the app-server boundary. Core
|
||||
//! should receive the behavior switches it needs, not the raw client name and
|
||||
//! version that led to those switches.
|
||||
|
||||
pub use codex_mcp::McpElicitationCompatibility;
|
||||
|
||||
/// Behavior switches selected by the app-server layer for known client quirks.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct ClientCompatibilityFlags {
|
||||
/// MCP elicitation capability policy to apply when starting or refreshing MCP servers.
|
||||
pub mcp_elicitation: McpElicitationCompatibility,
|
||||
}
|
||||
@@ -6447,6 +6447,7 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
|
||||
check_for_update_on_startup: true,
|
||||
disable_paste_burst: false,
|
||||
tui_notifications: Default::default(),
|
||||
client_compatibility_flags: Default::default(),
|
||||
animations: true,
|
||||
show_tooltips: true,
|
||||
tui_vim_mode_default: false,
|
||||
@@ -6649,6 +6650,7 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
|
||||
check_for_update_on_startup: true,
|
||||
disable_paste_burst: false,
|
||||
tui_notifications: Default::default(),
|
||||
client_compatibility_flags: Default::default(),
|
||||
animations: true,
|
||||
show_tooltips: true,
|
||||
tui_vim_mode_default: false,
|
||||
@@ -6805,6 +6807,7 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
|
||||
check_for_update_on_startup: true,
|
||||
disable_paste_burst: false,
|
||||
tui_notifications: Default::default(),
|
||||
client_compatibility_flags: Default::default(),
|
||||
animations: true,
|
||||
show_tooltips: true,
|
||||
tui_vim_mode_default: false,
|
||||
@@ -6946,6 +6949,7 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
|
||||
check_for_update_on_startup: true,
|
||||
disable_paste_burst: false,
|
||||
tui_notifications: Default::default(),
|
||||
client_compatibility_flags: Default::default(),
|
||||
animations: true,
|
||||
show_tooltips: true,
|
||||
tui_vim_mode_default: false,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::ClientCompatibilityFlags;
|
||||
use crate::agents_md::AgentsMdManager;
|
||||
use crate::config::edit::ConfigEdit;
|
||||
use crate::config::edit::ConfigEditsBuilder;
|
||||
@@ -505,6 +506,10 @@ pub struct Config {
|
||||
/// TUI notification settings, including enabled events, delivery method, and focus condition.
|
||||
pub tui_notifications: TuiNotificationSettings,
|
||||
|
||||
/// Runtime-only client compatibility flags selected by the app server
|
||||
/// before creating a session. These are not loaded from config.toml.
|
||||
pub client_compatibility_flags: ClientCompatibilityFlags,
|
||||
|
||||
/// Enable ASCII animations and shimmer effects in the TUI.
|
||||
pub animations: bool,
|
||||
|
||||
@@ -3135,6 +3140,7 @@ impl Config {
|
||||
.as_ref()
|
||||
.map(|t| t.notification_settings.clone())
|
||||
.unwrap_or_default(),
|
||||
client_compatibility_flags: ClientCompatibilityFlags::default(),
|
||||
animations: cfg.tui.as_ref().map(|t| t.animations).unwrap_or(true),
|
||||
show_tooltips: cfg.tui.as_ref().map(|t| t.show_tooltips).unwrap_or(true),
|
||||
model_availability_nux: cfg
|
||||
|
||||
@@ -280,6 +280,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
||||
codex_apps_tools_cache_key(auth.as_ref()),
|
||||
ToolPluginProvenance::default(),
|
||||
auth.as_ref(),
|
||||
codex_mcp::McpElicitationCompatibility::default(),
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ mod apps;
|
||||
mod arc_monitor;
|
||||
mod client;
|
||||
mod client_common;
|
||||
mod client_compatibility;
|
||||
mod realtime_context;
|
||||
mod realtime_conversation;
|
||||
mod realtime_prompt;
|
||||
@@ -110,6 +111,8 @@ pub mod test_support;
|
||||
mod unified_exec;
|
||||
pub mod windows_sandbox;
|
||||
pub use client::X_RESPONSESAPI_INCLUDE_TIMING_METRICS_HEADER;
|
||||
pub use client_compatibility::ClientCompatibilityFlags;
|
||||
pub use client_compatibility::McpElicitationCompatibility;
|
||||
pub use codex_protocol::config_types::ModelProviderAuthInfo;
|
||||
mod event_mapping;
|
||||
pub mod review_format;
|
||||
|
||||
@@ -1099,7 +1099,12 @@ async fn maybe_request_mcp_tool_approval(
|
||||
let tool_call_mcp_elicitation_enabled = turn_context
|
||||
.config
|
||||
.features
|
||||
.enabled(Feature::ToolCallMcpElicitation);
|
||||
.enabled(Feature::ToolCallMcpElicitation)
|
||||
&& turn_context
|
||||
.config
|
||||
.client_compatibility_flags
|
||||
.mcp_elicitation
|
||||
.allows_server_elicitation(&invocation.server);
|
||||
|
||||
if routes_approval_to_guardian(turn_context) {
|
||||
let review_id = new_guardian_review_id();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::McpElicitationCompatibility;
|
||||
use crate::config::ConfigBuilder;
|
||||
use crate::session::tests::make_session_and_context;
|
||||
use crate::session::tests::make_session_and_context_with_rx;
|
||||
@@ -18,6 +19,7 @@ use codex_hooks::HooksConfig;
|
||||
use codex_model_provider::create_model_provider;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::GranularApprovalConfig;
|
||||
use core_test_support::PathExt;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
@@ -2276,7 +2278,18 @@ async fn guardian_mode_mcp_denial_returns_rationale_message() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn prompt_mode_waits_for_approval_when_annotations_do_not_require_approval() {
|
||||
let (session, turn_context, _rx_event) = make_session_and_context_with_rx().await;
|
||||
let (session, mut turn_context, rx_event) = make_session_and_context_with_rx().await;
|
||||
{
|
||||
let turn_context = Arc::get_mut(&mut turn_context).expect("single turn context ref");
|
||||
let mut config = (*turn_context.config).clone();
|
||||
config
|
||||
.features
|
||||
.enable(Feature::ToolCallMcpElicitation)
|
||||
.expect("test setup should allow enabling tool call MCP elicitation");
|
||||
config.client_compatibility_flags.mcp_elicitation =
|
||||
McpElicitationCompatibility::CodexAppsOnly;
|
||||
turn_context.config = Arc::new(config);
|
||||
}
|
||||
{
|
||||
let mut active_turn = session.active_turn.lock().await;
|
||||
*active_turn = Some(ActiveTurn::default());
|
||||
@@ -2302,7 +2315,7 @@ async fn prompt_mode_waits_for_approval_when_annotations_do_not_require_approval
|
||||
openai_file_input_params: None,
|
||||
};
|
||||
|
||||
let mut approval_task = {
|
||||
let approval_task = {
|
||||
let session = Arc::clone(&session);
|
||||
let turn_context = Arc::clone(&turn_context);
|
||||
tokio::spawn(async move {
|
||||
@@ -2319,12 +2332,11 @@ async fn prompt_mode_waits_for_approval_when_annotations_do_not_require_approval
|
||||
})
|
||||
};
|
||||
|
||||
assert!(
|
||||
tokio::time::timeout(std::time::Duration::from_millis(200), &mut approval_task)
|
||||
.await
|
||||
.is_err(),
|
||||
"prompt mode should wait for approval instead of auto-allowing"
|
||||
);
|
||||
let event = tokio::time::timeout(std::time::Duration::from_secs(1), rx_event.recv())
|
||||
.await
|
||||
.expect("approval request should emit an event")
|
||||
.expect("event channel should stay open");
|
||||
assert!(matches!(event.msg, EventMsg::RequestUserInput(_)));
|
||||
approval_task.abort();
|
||||
}
|
||||
|
||||
|
||||
@@ -252,6 +252,10 @@ impl Session {
|
||||
codex_apps_tools_cache_key(auth.as_ref()),
|
||||
tool_plugin_provenance,
|
||||
auth.as_ref(),
|
||||
turn_context
|
||||
.config
|
||||
.client_compatibility_flags
|
||||
.mcp_elicitation,
|
||||
)
|
||||
.await;
|
||||
{
|
||||
|
||||
@@ -996,6 +996,10 @@ impl Session {
|
||||
codex_apps_tools_cache_key(auth),
|
||||
tool_plugin_provenance,
|
||||
auth,
|
||||
session_configuration
|
||||
.original_config_do_not_use
|
||||
.client_compatibility_flags
|
||||
.mcp_elicitation,
|
||||
)
|
||||
.instrument(info_span!(
|
||||
"session_init.mcp_manager_init",
|
||||
|
||||
Reference in New Issue
Block a user