mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Centralize thread MCP state in McpRuntime (#34930)
## What changed - Make `McpRuntime` own the published MCP configuration, connections, elicitation routing, and selected capability roots for a thread. - Capture immutable MCP bindings for model steps and tool calls so in-flight work keeps a consistent connection set and approval authority while refreshed state is published atomically. - Mark MCP state dirty when relevant configuration, plugins, environments, authentication, or elicitation capabilities change, then rebuild it before the next sampling request or out-of-band MCP operation. - Separate config reloads from server invalidation: `ReloadMcpConfig` applies resolved MCP inputs, while `RefreshMcpServers` requests reinitialization from the thread's latest state. ## Testing - Cover refreshed state visibility for existing turns, stable step bindings, current approval authority, cancelled refresh retries, resource-client reconciliation, and Apps recovery between sampling requests. GitOrigin-RevId: 59eabb1aa8dc083426bd18ef4d3630508f376401
This commit is contained in:
1 parent
39a2438d16
commit
e497325a6a
54 files changed
+1792
-2038
No files matched your search
+274
-276
@@ -10,7 +10,6 @@ use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::GuardianMcpAnnotations;
|
||||
use crate::guardian::new_guardian_review_id;
|
||||
use crate::guardian::review_approval_request;
|
||||
use crate::guardian::routes_approval_to_guardian_with_reviewer;
|
||||
use crate::hook_runtime::run_permission_request_hooks;
|
||||
use crate::mcp_openai_file::rewrite_mcp_tool_arguments_for_openai_files;
|
||||
use crate::mcp_tool_approval_templates::RenderedMcpToolApprovalParam;
|
||||
@@ -34,13 +33,12 @@ use codex_features::Feature;
|
||||
use codex_hooks::PermissionRequestDecision;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_mcp::MCP_TOOL_CODEX_APPS_META_KEY;
|
||||
use codex_mcp::McpConnectionSet;
|
||||
use codex_mcp::McpPermissionPromptAutoApproveContext;
|
||||
use codex_mcp::PreparedMcpCall;
|
||||
use codex_mcp::SandboxState;
|
||||
use codex_mcp::auth_elicitation_completed_result;
|
||||
use codex_mcp::build_auth_elicitation_plan;
|
||||
use codex_mcp::mcp_permission_prompt_is_auto_approved;
|
||||
use codex_mcp::tool_is_model_visible;
|
||||
use codex_protocol::approvals::ElicitationRequest;
|
||||
use codex_protocol::items::McpToolCallError;
|
||||
use codex_protocol::items::McpToolCallItem;
|
||||
@@ -119,7 +117,6 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
arguments: String,
|
||||
) -> HandledMcpToolCall {
|
||||
let turn_context = &step_context.turn;
|
||||
let manager = step_context.mcp.manager();
|
||||
// Parse the `arguments` as JSON. An empty string is OK, but invalid JSON
|
||||
// is not.
|
||||
let arguments_value = if arguments.trim().is_empty() {
|
||||
@@ -143,16 +140,14 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
arguments: arguments_value.clone(),
|
||||
};
|
||||
|
||||
let metadata = lookup_mcp_tool_metadata(
|
||||
sess.as_ref(),
|
||||
turn_context.as_ref(),
|
||||
manager,
|
||||
&server,
|
||||
&tool_name,
|
||||
)
|
||||
.await;
|
||||
let item_metadata = McpToolCallItemMetadata::from_tool_metadata(&server, metadata.as_ref());
|
||||
if metadata.is_none() {
|
||||
sess.refresh_mcp_if_dirty().await;
|
||||
let current_binding = sess.services.mcp_runtime.current_binding().await;
|
||||
let Some(prepared_call) = current_binding
|
||||
.as_ref()
|
||||
.and_then(|binding| binding.prepare_call(&server, &tool_name))
|
||||
else {
|
||||
let item_metadata =
|
||||
McpToolCallItemMetadata::from_tool_metadata(&server, /*metadata*/ None);
|
||||
let result = notify_mcp_tool_call_skip(
|
||||
sess.as_ref(),
|
||||
turn_context.as_ref(),
|
||||
@@ -168,48 +163,30 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
tool_input: arguments_value
|
||||
.unwrap_or_else(|| JsonValue::Object(serde_json::Map::new())),
|
||||
};
|
||||
}
|
||||
};
|
||||
let metadata = mcp_tool_metadata(&prepared_call);
|
||||
let item_metadata = McpToolCallItemMetadata::from_tool_metadata(&server, Some(&metadata));
|
||||
let runtime_config = prepared_call.config();
|
||||
let app_tool_policy = if server == CODEX_APPS_MCP_SERVER_NAME {
|
||||
let annotations = metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.annotations.as_ref());
|
||||
AppToolPolicyEvaluator::new(&turn_context.config.config_layer_stack).policy(
|
||||
AppToolPolicyInput {
|
||||
connector_id: metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.connector_id.as_deref()),
|
||||
tool_name: &tool_name,
|
||||
tool_title: metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.tool_title.as_deref()),
|
||||
destructive_hint: annotations.and_then(|annotations| annotations.destructive_hint),
|
||||
open_world_hint: annotations.and_then(|annotations| annotations.open_world_hint),
|
||||
},
|
||||
)
|
||||
let annotations = metadata.annotations.as_ref();
|
||||
AppToolPolicyEvaluator::new(&runtime_config.config_layer_stack).policy(AppToolPolicyInput {
|
||||
connector_id: metadata.connector_id.as_deref(),
|
||||
tool_name: &tool_name,
|
||||
tool_title: metadata.tool_title.as_deref(),
|
||||
destructive_hint: annotations.and_then(|annotations| annotations.destructive_hint),
|
||||
open_world_hint: annotations.and_then(|annotations| annotations.open_world_hint),
|
||||
})
|
||||
} else {
|
||||
AppToolPolicy::default()
|
||||
};
|
||||
let approval_mode = if server == CODEX_APPS_MCP_SERVER_NAME {
|
||||
app_tool_policy.approval
|
||||
} else if let Some(approval_mode) = {
|
||||
// Selected-plugin registrations are absent from config.toml and the legacy plugin manager,
|
||||
// so their resolved catalog entry is the authoritative source for tool approval policy.
|
||||
manager
|
||||
.is_selected_plugin_mcp_server(&server)
|
||||
.then(|| manager.tool_approval_mode(&server, &tool_name))
|
||||
} {
|
||||
approval_mode
|
||||
} else {
|
||||
custom_mcp_tool_approval_mode(sess.as_ref(), turn_context.as_ref(), &server, &tool_name)
|
||||
.await
|
||||
prepared_call.tool_approval_mode()
|
||||
};
|
||||
|
||||
let connector_id = metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.connector_id.clone());
|
||||
let connector_name = metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.connector_name.clone());
|
||||
let connector_id = metadata.connector_id.clone();
|
||||
let connector_name = metadata.connector_name.clone();
|
||||
|
||||
if server == CODEX_APPS_MCP_SERVER_NAME && !app_tool_policy.enabled {
|
||||
let result = notify_mcp_tool_call_skip(
|
||||
@@ -239,6 +216,8 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
.unwrap_or_else(|| JsonValue::Object(serde_json::Map::new())),
|
||||
};
|
||||
}
|
||||
sess.register_mcp_tool_approval_metadata(turn_context, &call_id, metadata.clone())
|
||||
.await;
|
||||
notify_mcp_tool_call_started(
|
||||
sess.as_ref(),
|
||||
turn_context.as_ref(),
|
||||
@@ -248,28 +227,39 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
)
|
||||
.await;
|
||||
|
||||
let approval_policy = if prepared_call.is_selected_plugin_server() {
|
||||
McpToolApprovalPolicy::for_selected_plugin(approval_mode)
|
||||
} else {
|
||||
McpToolApprovalPolicy::for_server(approval_mode)
|
||||
};
|
||||
if let Some(decision) = maybe_request_mcp_tool_approval(
|
||||
&sess,
|
||||
step_context,
|
||||
&call_id,
|
||||
&invocation,
|
||||
&hook_tool_name,
|
||||
metadata.as_ref(),
|
||||
approval_mode,
|
||||
&metadata,
|
||||
prepared_call.config(),
|
||||
approval_policy,
|
||||
)
|
||||
.await
|
||||
{
|
||||
let result = match decision {
|
||||
McpToolApprovalDecision::Accept
|
||||
decision @ (McpToolApprovalDecision::Accept
|
||||
| McpToolApprovalDecision::AcceptForSession
|
||||
| McpToolApprovalDecision::AcceptAndRemember => {
|
||||
| McpToolApprovalDecision::AcceptAndRemember) => {
|
||||
return handle_approved_mcp_tool_call(
|
||||
sess.as_ref(),
|
||||
&sess,
|
||||
step_context.as_ref(),
|
||||
&call_id,
|
||||
invocation,
|
||||
metadata.as_ref(),
|
||||
prepared_call,
|
||||
metadata,
|
||||
item_metadata,
|
||||
McpToolApprovalApplication::Apply {
|
||||
decision,
|
||||
policy: approval_policy,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -321,12 +311,14 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
}
|
||||
|
||||
handle_approved_mcp_tool_call(
|
||||
sess.as_ref(),
|
||||
&sess,
|
||||
step_context.as_ref(),
|
||||
&call_id,
|
||||
invocation,
|
||||
metadata.as_ref(),
|
||||
prepared_call,
|
||||
metadata,
|
||||
item_metadata,
|
||||
McpToolApprovalApplication::NotRequired,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -372,53 +364,116 @@ impl McpToolCallItemMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "MCP approval must be applied inside the prepared call's catalog lease"
|
||||
)]
|
||||
async fn handle_approved_mcp_tool_call(
|
||||
sess: &Session,
|
||||
sess: &Arc<Session>,
|
||||
step_context: &StepContext,
|
||||
call_id: &str,
|
||||
invocation: McpInvocation,
|
||||
metadata: Option<&McpToolApprovalMetadata>,
|
||||
prepared_call: PreparedMcpCall,
|
||||
metadata: McpToolApprovalMetadata,
|
||||
item_metadata: McpToolCallItemMetadata,
|
||||
approval_application: McpToolApprovalApplication,
|
||||
) -> HandledMcpToolCall {
|
||||
let turn_context = step_context.turn.as_ref();
|
||||
let manager = step_context.mcp.manager();
|
||||
let server = invocation.server.clone();
|
||||
maybe_mark_thread_memory_mode_polluted(sess, turn_context, manager, &server).await;
|
||||
let tool_name = invocation.tool.clone();
|
||||
let arguments_value = invocation.arguments.clone();
|
||||
let connector_id = metadata.and_then(|metadata| metadata.connector_id.as_deref());
|
||||
let connector_name = metadata.and_then(|metadata| metadata.connector_name.as_deref());
|
||||
let server_origin = manager.server_origin(&server).map(str::to_string);
|
||||
let connector_id = metadata.connector_id.as_deref();
|
||||
let connector_name = metadata.connector_name.as_deref();
|
||||
let server_origin = prepared_call.server_origin().map(str::to_string);
|
||||
|
||||
let start = Instant::now();
|
||||
let rewrite = rewrite_mcp_tool_arguments_for_openai_files(
|
||||
sess,
|
||||
turn_context,
|
||||
arguments_value.clone(),
|
||||
metadata.and_then(|metadata| metadata.openai_file_input_optional_fields.as_ref()),
|
||||
)
|
||||
.await;
|
||||
let tool_input = match &rewrite {
|
||||
Ok(Some(rewritten_arguments)) => rewritten_arguments.clone(),
|
||||
Ok(None) | Err(_) => arguments_value
|
||||
.clone()
|
||||
.unwrap_or_else(|| JsonValue::Object(serde_json::Map::new())),
|
||||
};
|
||||
let mut tool_input = arguments_value
|
||||
.clone()
|
||||
.unwrap_or_else(|| JsonValue::Object(serde_json::Map::new()));
|
||||
let result = async {
|
||||
let result = async {
|
||||
let rewritten_arguments = rewrite?;
|
||||
let request_meta =
|
||||
build_mcp_tool_call_request_meta(turn_context, &server, call_id, metadata);
|
||||
execute_mcp_tool_call(
|
||||
let result = prepared_call
|
||||
.call_with_preparation(|| async {
|
||||
if let McpToolApprovalApplication::Apply { decision, policy } =
|
||||
&approval_application
|
||||
{
|
||||
let session_approval_key = session_mcp_tool_approval_key(
|
||||
&invocation,
|
||||
Some(&metadata),
|
||||
policy.mode,
|
||||
);
|
||||
let persistent_approval_key = if policy.allow_persistent {
|
||||
persistent_mcp_tool_approval_key(
|
||||
&invocation,
|
||||
Some(&metadata),
|
||||
policy.mode,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
apply_mcp_tool_approval_decision(
|
||||
sess,
|
||||
turn_context,
|
||||
decision,
|
||||
session_approval_key,
|
||||
persistent_approval_key,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
maybe_mark_thread_memory_mode_polluted(sess, turn_context, &prepared_call)
|
||||
.await;
|
||||
let rewritten_arguments = rewrite_mcp_tool_arguments_for_openai_files(
|
||||
sess,
|
||||
turn_context,
|
||||
arguments_value,
|
||||
metadata.openai_file_input_optional_fields.as_ref(),
|
||||
)
|
||||
.await
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
if let Some(rewritten_arguments) = rewritten_arguments.as_ref() {
|
||||
tool_input = rewritten_arguments.clone();
|
||||
}
|
||||
let request_meta = build_mcp_tool_call_request_meta(
|
||||
turn_context,
|
||||
&server,
|
||||
call_id,
|
||||
Some(&metadata),
|
||||
);
|
||||
let request_meta = with_mcp_tool_call_thread_id_meta(
|
||||
request_meta,
|
||||
&sess.thread_id.to_string(),
|
||||
);
|
||||
let request_meta = augment_mcp_tool_request_meta_with_sandbox_state(
|
||||
step_context,
|
||||
&prepared_call,
|
||||
request_meta,
|
||||
)
|
||||
.await?;
|
||||
let mcp_call_trace = sess
|
||||
.services
|
||||
.rollout_thread_trace
|
||||
.start_mcp_call_trace(call_id);
|
||||
Ok((
|
||||
rewritten_arguments,
|
||||
mcp_call_trace.add_request_meta(request_meta),
|
||||
))
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("tool call error: {error:?}"))?;
|
||||
let result = sanitize_mcp_tool_result_for_model(
|
||||
&turn_context.model_info.input_modalities,
|
||||
Ok(result),
|
||||
)?;
|
||||
Ok(maybe_request_codex_apps_auth_elicitation(
|
||||
sess,
|
||||
step_context,
|
||||
turn_context,
|
||||
prepared_call.config().approval_policy.value(),
|
||||
call_id,
|
||||
&invocation,
|
||||
rewritten_arguments,
|
||||
metadata,
|
||||
request_meta,
|
||||
&invocation.server,
|
||||
Some(&metadata),
|
||||
result,
|
||||
)
|
||||
.await
|
||||
.await)
|
||||
}
|
||||
.await;
|
||||
record_mcp_result_span_telemetry(&Span::current(), &result);
|
||||
@@ -451,7 +506,7 @@ async fn handle_approved_mcp_tool_call(
|
||||
truncate_mcp_tool_result_for_event(&result),
|
||||
)
|
||||
.await;
|
||||
maybe_track_codex_app_used(sess, turn_context, manager, &server, &tool_name).await;
|
||||
maybe_track_codex_app_used(sess, turn_context, &server, &metadata).await;
|
||||
|
||||
let outcome = mcp_call_metric_outcome(&result);
|
||||
emit_mcp_call_metrics(
|
||||
@@ -576,64 +631,16 @@ fn truncate_str_to_char_boundary(value: &str, max_chars: usize) -> &str {
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_mcp_tool_call(
|
||||
sess: &Session,
|
||||
step_context: &StepContext,
|
||||
call_id: &str,
|
||||
invocation: &McpInvocation,
|
||||
rewritten_arguments: Option<JsonValue>,
|
||||
metadata: Option<&McpToolApprovalMetadata>,
|
||||
request_meta: Option<JsonValue>,
|
||||
) -> Result<CallToolResult, String> {
|
||||
let turn_context = step_context.turn.as_ref();
|
||||
let manager = step_context.mcp.manager();
|
||||
let request_meta = with_mcp_tool_call_thread_id_meta(request_meta, &sess.thread_id.to_string());
|
||||
let request_meta = augment_mcp_tool_request_meta_with_sandbox_state(
|
||||
step_context,
|
||||
manager,
|
||||
&invocation.server,
|
||||
request_meta,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("failed to build MCP tool request metadata: {e:#}"))?;
|
||||
let mcp_call_trace = sess
|
||||
.services
|
||||
.rollout_thread_trace
|
||||
.start_mcp_call_trace(call_id);
|
||||
let request_meta = mcp_call_trace.add_request_meta(request_meta);
|
||||
let result = manager
|
||||
.call_tool(
|
||||
&invocation.server,
|
||||
&invocation.tool,
|
||||
rewritten_arguments,
|
||||
request_meta,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("tool call error: {e:?}"))?;
|
||||
let result =
|
||||
sanitize_mcp_tool_result_for_model(&turn_context.model_info.input_modalities, Ok(result))?;
|
||||
Ok(maybe_request_codex_apps_auth_elicitation(
|
||||
sess,
|
||||
turn_context,
|
||||
manager,
|
||||
call_id,
|
||||
&invocation.server,
|
||||
metadata,
|
||||
result,
|
||||
)
|
||||
.await)
|
||||
}
|
||||
|
||||
async fn maybe_request_codex_apps_auth_elicitation(
|
||||
sess: &Session,
|
||||
sess: &Arc<Session>,
|
||||
turn_context: &TurnContext,
|
||||
manager: &McpConnectionSet,
|
||||
approval_policy: AskForApproval,
|
||||
call_id: &str,
|
||||
server: &str,
|
||||
metadata: Option<&McpToolApprovalMetadata>,
|
||||
result: CallToolResult,
|
||||
) -> CallToolResult {
|
||||
if !manager.is_host_owned_codex_apps_server(server) {
|
||||
if server != CODEX_APPS_MCP_SERVER_NAME {
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -645,7 +652,7 @@ async fn maybe_request_codex_apps_auth_elicitation(
|
||||
return result;
|
||||
}
|
||||
|
||||
match turn_context.approval_policy.value() {
|
||||
match approval_policy {
|
||||
AskForApproval::Never => return result,
|
||||
AskForApproval::Granular(granular_config) if !granular_config.allows_mcp_elicitations() => {
|
||||
return result;
|
||||
@@ -691,16 +698,12 @@ async fn maybe_request_codex_apps_auth_elicitation(
|
||||
return result;
|
||||
}
|
||||
|
||||
refresh_codex_apps_after_connector_auth(sess, turn_context, manager).await;
|
||||
refresh_codex_apps_after_connector_auth(sess, turn_context).await;
|
||||
auth_elicitation_completed_result(&plan.auth_failure, result.meta)
|
||||
}
|
||||
|
||||
async fn refresh_codex_apps_after_connector_auth(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
manager: &McpConnectionSet,
|
||||
) {
|
||||
let mcp_tools_result = manager.hard_refresh_codex_apps_tools_cache().await;
|
||||
async fn refresh_codex_apps_after_connector_auth(sess: &Arc<Session>, turn_context: &TurnContext) {
|
||||
let mcp_tools_result = sess.hard_refresh_latest_codex_apps_tools().await;
|
||||
|
||||
match mcp_tools_result {
|
||||
Ok(mcp_tools) => {
|
||||
@@ -719,31 +722,33 @@ async fn refresh_codex_apps_after_connector_auth(
|
||||
|
||||
async fn augment_mcp_tool_request_meta_with_sandbox_state(
|
||||
step_context: &StepContext,
|
||||
manager: &McpConnectionSet,
|
||||
server: &str,
|
||||
prepared_call: &PreparedMcpCall,
|
||||
mut meta: Option<serde_json::Value>,
|
||||
) -> anyhow::Result<Option<serde_json::Value>> {
|
||||
let turn_context = step_context.turn.as_ref();
|
||||
let supports_sandbox_state_meta = manager
|
||||
.server_supports_sandbox_state_meta_capability(server)
|
||||
let supports_sandbox_state_meta = prepared_call
|
||||
.server_supports_sandbox_state_meta_capability()
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
if !supports_sandbox_state_meta {
|
||||
return Ok(meta);
|
||||
}
|
||||
|
||||
let server_environment_id = manager
|
||||
.server_environment_id(server)
|
||||
.unwrap_or(codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID);
|
||||
let Some(sandbox_cwd) = sandbox_cwd_for_mcp_server(step_context, server_environment_id) else {
|
||||
let server_environment_id = prepared_call.server_environment_id();
|
||||
let Some(sandbox_cwd) = prepared_call
|
||||
.config()
|
||||
.environment_cwds
|
||||
.get(server_environment_id)
|
||||
.cloned()
|
||||
.or_else(|| sandbox_cwd_for_mcp_server(step_context, server_environment_id))
|
||||
else {
|
||||
return Ok(meta);
|
||||
};
|
||||
let permission_profile = turn_context.permission_profile();
|
||||
let permission_profile = prepared_call.config().permission_profile.clone();
|
||||
let sandbox_state = serde_json::to_value(SandboxState {
|
||||
permission_profile,
|
||||
codex_linux_sandbox_exe: step_context.mcp.config().codex_linux_sandbox_exe.clone(),
|
||||
codex_linux_sandbox_exe: prepared_call.config().codex_linux_sandbox_exe.clone(),
|
||||
sandbox_cwd,
|
||||
use_legacy_landlock: step_context.mcp.config().use_legacy_landlock,
|
||||
use_legacy_landlock: prepared_call.config().use_legacy_landlock,
|
||||
})?;
|
||||
|
||||
match meta.as_mut() {
|
||||
@@ -787,14 +792,12 @@ fn sandbox_cwd_for_mcp_server(step_context: &StepContext, environment_id: &str)
|
||||
async fn maybe_mark_thread_memory_mode_polluted(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
manager: &McpConnectionSet,
|
||||
server: &str,
|
||||
prepared_call: &PreparedMcpCall,
|
||||
) {
|
||||
if !turn_context.config.memories.disable_on_external_context {
|
||||
return;
|
||||
}
|
||||
let pollutes_memory = manager.server_pollutes_memory(server);
|
||||
if !pollutes_memory {
|
||||
if !prepared_call.server_pollutes_memory() {
|
||||
return;
|
||||
}
|
||||
state_db::mark_thread_memory_mode_polluted(
|
||||
@@ -963,25 +966,17 @@ async fn notify_mcp_tool_call_completed(
|
||||
sess.emit_turn_item_completed(turn_context, item).await;
|
||||
}
|
||||
|
||||
struct McpAppUsageMetadata {
|
||||
connector_id: Option<String>,
|
||||
app_name: Option<String>,
|
||||
}
|
||||
|
||||
async fn maybe_track_codex_app_used(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
manager: &McpConnectionSet,
|
||||
server: &str,
|
||||
tool_name: &str,
|
||||
metadata: &McpToolApprovalMetadata,
|
||||
) {
|
||||
if server != CODEX_APPS_MCP_SERVER_NAME {
|
||||
return;
|
||||
}
|
||||
let metadata = lookup_mcp_app_usage_metadata(manager, server, tool_name).await;
|
||||
let (connector_id, app_name) = metadata
|
||||
.map(|metadata| (metadata.connector_id, metadata.app_name))
|
||||
.unwrap_or((None, None));
|
||||
let connector_id = metadata.connector_id.clone();
|
||||
let app_name = metadata.connector_name.clone();
|
||||
let invocation_type = if let Some(connector_id) = connector_id.as_deref() {
|
||||
let mentioned_connector_ids = sess.get_connector_selection().await;
|
||||
if mentioned_connector_ids.contains(connector_id) {
|
||||
@@ -1018,6 +1013,36 @@ enum McpToolApprovalDecision {
|
||||
Cancel,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct McpToolApprovalPolicy {
|
||||
mode: AppToolApproval,
|
||||
allow_persistent: bool,
|
||||
}
|
||||
|
||||
enum McpToolApprovalApplication {
|
||||
NotRequired,
|
||||
Apply {
|
||||
decision: McpToolApprovalDecision,
|
||||
policy: McpToolApprovalPolicy,
|
||||
},
|
||||
}
|
||||
|
||||
impl McpToolApprovalPolicy {
|
||||
fn for_server(mode: AppToolApproval) -> Self {
|
||||
Self {
|
||||
mode,
|
||||
allow_persistent: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn for_selected_plugin(mode: AppToolApproval) -> Self {
|
||||
Self {
|
||||
mode,
|
||||
allow_persistent: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct McpToolApprovalMetadata {
|
||||
annotations: Option<ToolAnnotations>,
|
||||
@@ -1034,6 +1059,40 @@ pub(crate) struct McpToolApprovalMetadata {
|
||||
openai_file_input_optional_fields: Option<HashMap<String, Vec<String>>>,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
async fn register_mcp_tool_approval_metadata(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
call_id: &str,
|
||||
metadata: McpToolApprovalMetadata,
|
||||
) {
|
||||
let Some(turn_state) = self
|
||||
.input_queue
|
||||
.turn_state_for_sub_id(&self.active_turn, &turn_context.sub_id)
|
||||
.await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
turn_state
|
||||
.lock()
|
||||
.await
|
||||
.insert_mcp_tool_approval_metadata(call_id.to_string(), metadata);
|
||||
}
|
||||
|
||||
pub(crate) async fn mcp_tool_approval_metadata(
|
||||
&self,
|
||||
sub_id: &str,
|
||||
call_id: &str,
|
||||
) -> Option<McpToolApprovalMetadata> {
|
||||
let turn_state = self
|
||||
.input_queue
|
||||
.turn_state_for_sub_id(&self.active_turn, sub_id)
|
||||
.await?;
|
||||
|
||||
turn_state.lock().await.mcp_tool_approval_metadata(call_id)
|
||||
}
|
||||
}
|
||||
|
||||
const MCP_TOOL_OPENAI_OUTPUT_TEMPLATE_META_KEY: &str = "openai/outputTemplate";
|
||||
const MCP_TOOL_UI_RESOURCE_URI_META_KEY: &str = "ui/resourceUri";
|
||||
const MCP_TOOL_LINK_ID_META_KEY: &str = "link_id";
|
||||
@@ -1042,6 +1101,7 @@ const MCP_TOOL_THREAD_ID_META_KEY: &str = "threadId";
|
||||
const MCP_TOOL_CONNECTED_ACCOUNT_EMAIL_META_KEY: &str = "connected_account_email";
|
||||
const MCP_TOOL_RESOURCE_URI_META_KEY: &str = "resource_uri";
|
||||
|
||||
#[cfg(test)]
|
||||
async fn custom_mcp_tool_approval_mode(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
@@ -1211,38 +1271,45 @@ fn mcp_tool_approval_prompt_options(
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
async fn maybe_request_mcp_tool_approval(
|
||||
sess: &Arc<Session>,
|
||||
step_context: &Arc<StepContext>,
|
||||
call_id: &str,
|
||||
invocation: &McpInvocation,
|
||||
hook_tool_name: &HookToolName,
|
||||
metadata: Option<&McpToolApprovalMetadata>,
|
||||
approval_mode: AppToolApproval,
|
||||
metadata: &McpToolApprovalMetadata,
|
||||
config: &codex_mcp::McpConfig,
|
||||
policy: McpToolApprovalPolicy,
|
||||
) -> Option<McpToolApprovalDecision> {
|
||||
let turn_context = &step_context.turn;
|
||||
let manager = step_context.mcp.manager();
|
||||
let approvals_reviewer = mcp_approvals_reviewer(turn_context, &invocation.server, metadata);
|
||||
let approvals_reviewer = connectors::mcp_approvals_reviewer_from_layers(
|
||||
&config.config_layer_stack,
|
||||
config.approvals_reviewer,
|
||||
&invocation.server,
|
||||
metadata.connector_id.as_deref(),
|
||||
);
|
||||
if mcp_permission_prompt_is_auto_approved(
|
||||
turn_context.approval_policy.value(),
|
||||
&turn_context.permission_profile(),
|
||||
config.approval_policy.value(),
|
||||
&config.permission_profile,
|
||||
McpPermissionPromptAutoApproveContext {
|
||||
tool_approval_mode: Some(approval_mode),
|
||||
tool_approval_mode: Some(policy.mode),
|
||||
},
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let annotations = metadata.and_then(|metadata| metadata.annotations.as_ref());
|
||||
if !requires_mcp_tool_approval_for_mode(annotations, approval_mode) {
|
||||
let annotations = metadata.annotations.as_ref();
|
||||
if !requires_mcp_tool_approval_for_mode(annotations, policy.mode) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let session_approval_key = session_mcp_tool_approval_key(invocation, metadata, approval_mode);
|
||||
let persistent_approval_key = if manager.is_selected_plugin_mcp_server(&invocation.server) {
|
||||
None
|
||||
let session_approval_key =
|
||||
session_mcp_tool_approval_key(invocation, Some(metadata), policy.mode);
|
||||
let persistent_approval_key = if policy.allow_persistent {
|
||||
persistent_mcp_tool_approval_key(invocation, Some(metadata), policy.mode)
|
||||
} else {
|
||||
persistent_mcp_tool_approval_key(invocation, metadata, approval_mode)
|
||||
None
|
||||
};
|
||||
if let Some(key) = session_approval_key.as_ref()
|
||||
&& mcp_tool_approval_is_remembered(sess, key).await
|
||||
@@ -1280,25 +1347,21 @@ async fn maybe_request_mcp_tool_approval(
|
||||
.features
|
||||
.enabled(Feature::ToolCallMcpElicitation);
|
||||
|
||||
if routes_approval_to_guardian_with_reviewer(turn_context, approvals_reviewer) {
|
||||
if matches!(
|
||||
config.approval_policy.value(),
|
||||
AskForApproval::OnRequest | AskForApproval::Granular(_)
|
||||
) && approvals_reviewer == ApprovalsReviewer::AutoReview
|
||||
{
|
||||
let review_id = new_guardian_review_id();
|
||||
let decision = review_approval_request(
|
||||
sess,
|
||||
turn_context,
|
||||
review_id.clone(),
|
||||
build_guardian_mcp_tool_review_request(call_id, invocation, metadata),
|
||||
build_guardian_mcp_tool_review_request(call_id, invocation, Some(metadata)),
|
||||
/*retry_reason*/ None,
|
||||
)
|
||||
.await;
|
||||
let decision = mcp_tool_approval_decision_from_guardian(decision);
|
||||
apply_mcp_tool_approval_decision(
|
||||
sess,
|
||||
turn_context,
|
||||
&decision,
|
||||
session_approval_key,
|
||||
persistent_approval_key,
|
||||
)
|
||||
.await;
|
||||
return Some(decision);
|
||||
}
|
||||
|
||||
@@ -1310,9 +1373,9 @@ async fn maybe_request_mcp_tool_approval(
|
||||
let question_id = format!("{MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX}_{call_id}");
|
||||
let rendered_template = render_mcp_tool_approval_template(
|
||||
&invocation.server,
|
||||
metadata.and_then(|metadata| metadata.connector_id.as_deref()),
|
||||
metadata.and_then(|metadata| metadata.connector_name.as_deref()),
|
||||
metadata.and_then(|metadata| metadata.tool_title.as_deref()),
|
||||
metadata.connector_id.as_deref(),
|
||||
metadata.connector_name.as_deref(),
|
||||
metadata.tool_title.as_deref(),
|
||||
invocation.arguments.as_ref(),
|
||||
);
|
||||
let tool_params_display = rendered_template
|
||||
@@ -1323,7 +1386,7 @@ async fn maybe_request_mcp_tool_approval(
|
||||
question_id.clone(),
|
||||
&invocation.server,
|
||||
&invocation.tool,
|
||||
metadata.and_then(|metadata| metadata.connector_name.as_deref()),
|
||||
metadata.connector_name.as_deref(),
|
||||
prompt_options,
|
||||
rendered_template
|
||||
.as_ref()
|
||||
@@ -1336,7 +1399,7 @@ async fn maybe_request_mcp_tool_approval(
|
||||
let request =
|
||||
build_mcp_tool_approval_elicitation_request(McpToolApprovalElicitationRequest {
|
||||
server: &invocation.server,
|
||||
metadata,
|
||||
metadata: Some(metadata),
|
||||
tool_params: rendered_template
|
||||
.as_ref()
|
||||
.and_then(|rendered_template| rendered_template.tool_params.as_ref())
|
||||
@@ -1359,15 +1422,7 @@ async fn maybe_request_mcp_tool_approval(
|
||||
.response,
|
||||
&question_id,
|
||||
);
|
||||
let decision = normalize_approval_decision_for_mode(decision, approval_mode);
|
||||
apply_mcp_tool_approval_decision(
|
||||
sess,
|
||||
turn_context,
|
||||
&decision,
|
||||
session_approval_key,
|
||||
persistent_approval_key,
|
||||
)
|
||||
.await;
|
||||
let decision = normalize_approval_decision_for_mode(decision, policy.mode);
|
||||
return Some(decision);
|
||||
}
|
||||
|
||||
@@ -1380,16 +1435,8 @@ async fn maybe_request_mcp_tool_approval(
|
||||
.await;
|
||||
let decision = normalize_approval_decision_for_mode(
|
||||
parse_mcp_tool_approval_response(response, &question_id),
|
||||
approval_mode,
|
||||
policy.mode,
|
||||
);
|
||||
apply_mcp_tool_approval_decision(
|
||||
sess,
|
||||
turn_context,
|
||||
&decision,
|
||||
session_approval_key,
|
||||
persistent_approval_key,
|
||||
)
|
||||
.await;
|
||||
Some(decision)
|
||||
}
|
||||
|
||||
@@ -1478,49 +1525,12 @@ fn mcp_tool_approval_decision_from_guardian(decision: ReviewDecision) -> McpTool
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn lookup_mcp_tool_metadata(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
manager: &McpConnectionSet,
|
||||
server: &str,
|
||||
tool_name: &str,
|
||||
) -> Option<McpToolApprovalMetadata> {
|
||||
let plugin_id = manager
|
||||
.plugin_id_for_mcp_server_name(server)
|
||||
.map(str::to_string);
|
||||
let tool_info = manager.tool_info(server, tool_name).await?;
|
||||
if !tool_is_model_visible(&tool_info) {
|
||||
return None;
|
||||
}
|
||||
let connector_description = if server == CODEX_APPS_MCP_SERVER_NAME {
|
||||
let connectors = match connectors::list_cached_accessible_connectors_from_mcp_tools(
|
||||
turn_context.config.as_ref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Some(connectors) => Some(connectors),
|
||||
None => {
|
||||
connectors::list_accessible_connectors_from_mcp_tools_with_mcp_manager(
|
||||
turn_context.config.as_ref(),
|
||||
/*force_refetch*/ false,
|
||||
sess.services.turn_environments.environment_manager(),
|
||||
Arc::clone(&sess.services.mcp_manager),
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.map(|status| status.connectors)
|
||||
}
|
||||
};
|
||||
connectors.and_then(|connectors| {
|
||||
let connector_id = tool_info.connector_id.as_deref()?;
|
||||
connectors
|
||||
.into_iter()
|
||||
.find(|connector| connector.id == connector_id)
|
||||
.and_then(|connector| connector.description)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
fn mcp_tool_metadata(prepared_call: &PreparedMcpCall) -> McpToolApprovalMetadata {
|
||||
let server = prepared_call.server_name();
|
||||
let tool_info = prepared_call.tool_info().clone();
|
||||
let connector_description = (server == CODEX_APPS_MCP_SERVER_NAME)
|
||||
.then(|| tool_info.namespace_description.clone())
|
||||
.flatten();
|
||||
|
||||
let codex_apps_meta = tool_info
|
||||
.tool
|
||||
@@ -1541,7 +1551,7 @@ pub(crate) async fn lookup_mcp_tool_metadata(
|
||||
None
|
||||
};
|
||||
|
||||
Some(McpToolApprovalMetadata {
|
||||
McpToolApprovalMetadata {
|
||||
annotations: tool_info.tool.annotations,
|
||||
connector_id: tool_info.connector_id,
|
||||
link_id: tool_info
|
||||
@@ -1554,7 +1564,7 @@ pub(crate) async fn lookup_mcp_tool_metadata(
|
||||
connector_name: tool_info.connector_name,
|
||||
connector_description,
|
||||
connected_account_email,
|
||||
plugin_id,
|
||||
plugin_id: prepared_call.plugin_id().map(str::to_string),
|
||||
tool_title: tool_info.tool.title,
|
||||
tool_description: tool_info.tool.description.map(std::borrow::Cow::into_owned),
|
||||
mcp_app_resource_uri: get_mcp_app_resource_uri(tool_info.tool.meta.as_deref()),
|
||||
@@ -1564,7 +1574,7 @@ pub(crate) async fn lookup_mcp_tool_metadata(
|
||||
server,
|
||||
&tool_info.openai_file_input_optional_fields,
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn openai_file_input_optional_fields_for_server(
|
||||
@@ -1596,18 +1606,6 @@ fn get_mcp_app_resource_uri(
|
||||
})
|
||||
}
|
||||
|
||||
async fn lookup_mcp_app_usage_metadata(
|
||||
manager: &McpConnectionSet,
|
||||
server: &str,
|
||||
tool_name: &str,
|
||||
) -> Option<McpAppUsageMetadata> {
|
||||
let tool_info = manager.tool_info(server, tool_name).await?;
|
||||
Some(McpAppUsageMetadata {
|
||||
connector_id: tool_info.connector_id,
|
||||
app_name: tool_info.connector_name,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_mcp_tool_approval_question(
|
||||
question_id: String,
|
||||
server: &str,
|
||||
|
||||
Reference in new issue
Block a user