mirror of
https://github.com/openai/codex.git
synced 2026-10-28 08:04:55 +00:00
Add strict automatic review for MCP elicitations (#36365)
## What changed - Recognize the `codex_strict_auto_review` MCP elicitation marker and route marked approval requests through the configured automatic reviewer. - Accept only canonical automatic-review approvals and fail closed without a user prompt when review is unavailable, denied, malformed, disallowed by policy, or requests persistence. - Validate Codex Apps approval metadata against the active MCP invocation and build the review request from the trusted invocation details. ## Testing - Add unit coverage for strict review decisions, malformed markers, reviewer failures, explicit policy denials, and lifecycle cleanup. - Add app-server round-trip coverage for approvals, denials, configuration constraints, spoofed metadata, persistent requests, and subsequent turns. GitOrigin-RevId: af73b45a9b63118f25f6429d95b3222c5dbb59fb
This commit is contained in:
@@ -391,7 +391,8 @@ async fn forward_events(
|
||||
// runtime after a refresh.
|
||||
let metadata = session
|
||||
.mcp_tool_approval_metadata(&id, &event.call_id)
|
||||
.await;
|
||||
.await
|
||||
.map(|(_, metadata)| metadata);
|
||||
pending_mcp_invocations
|
||||
.lock()
|
||||
.await
|
||||
|
||||
@@ -211,7 +211,7 @@ 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())
|
||||
sess.register_mcp_tool_approval_metadata(turn_context, &call_id, &invocation, metadata.clone())
|
||||
.await;
|
||||
notify_mcp_tool_call_started(
|
||||
sess.as_ref(),
|
||||
@@ -1047,7 +1047,7 @@ impl McpToolApprovalPolicy {
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct McpToolApprovalMetadata {
|
||||
annotations: Option<ToolAnnotations>,
|
||||
connector_id: Option<String>,
|
||||
pub(crate) connector_id: Option<String>,
|
||||
link_id: Option<String>,
|
||||
connector_name: Option<String>,
|
||||
connector_description: Option<String>,
|
||||
@@ -1065,6 +1065,7 @@ impl Session {
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
call_id: &str,
|
||||
invocation: &McpInvocation,
|
||||
metadata: McpToolApprovalMetadata,
|
||||
) {
|
||||
let Some(turn_state) = self
|
||||
@@ -1074,17 +1075,18 @@ impl Session {
|
||||
else {
|
||||
return;
|
||||
};
|
||||
turn_state
|
||||
.lock()
|
||||
.await
|
||||
.insert_mcp_tool_approval_metadata(call_id.to_string(), metadata);
|
||||
turn_state.lock().await.insert_mcp_tool_approval_metadata(
|
||||
call_id.to_string(),
|
||||
(invocation.server == CODEX_APPS_MCP_SERVER_NAME).then(|| invocation.clone()),
|
||||
metadata,
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) async fn mcp_tool_approval_metadata(
|
||||
&self,
|
||||
sub_id: &str,
|
||||
call_id: &str,
|
||||
) -> Option<McpToolApprovalMetadata> {
|
||||
) -> Option<(Option<McpInvocation>, McpToolApprovalMetadata)> {
|
||||
let turn_state = self
|
||||
.input_queue
|
||||
.turn_state_for_sub_id(&self.active_turn, sub_id)
|
||||
|
||||
@@ -9,6 +9,7 @@ use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_mcp::ElicitationReviewRequest;
|
||||
use codex_mcp::ElicitationReviewer;
|
||||
use codex_mcp::ElicitationReviewerHandle;
|
||||
use codex_mcp::MCP_TOOL_CODEX_APPS_META_KEY;
|
||||
use codex_protocol::capabilities::CapabilityRootLocation;
|
||||
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
@@ -19,8 +20,10 @@ use codex_protocol::mcp_approval_meta::APPROVALS_REVIEWER_KEY as MCP_ELICITATION
|
||||
use codex_protocol::mcp_approval_meta::CONNECTOR_DESCRIPTION_KEY as MCP_ELICITATION_CONNECTOR_DESCRIPTION_KEY;
|
||||
use codex_protocol::mcp_approval_meta::CONNECTOR_ID_KEY as MCP_ELICITATION_CONNECTOR_ID_KEY;
|
||||
use codex_protocol::mcp_approval_meta::CONNECTOR_NAME_KEY as MCP_ELICITATION_CONNECTOR_NAME_KEY;
|
||||
use codex_protocol::mcp_approval_meta::PERSIST_KEY as MCP_ELICITATION_PERSIST_KEY;
|
||||
use codex_protocol::mcp_approval_meta::REQUEST_TYPE_APPROVAL_REQUEST as MCP_ELICITATION_REQUEST_TYPE_APPROVAL_REQUEST;
|
||||
use codex_protocol::mcp_approval_meta::REQUEST_TYPE_KEY as MCP_ELICITATION_REQUEST_TYPE_KEY;
|
||||
use codex_protocol::mcp_approval_meta::STRICT_AUTO_REVIEW_KEY as MCP_ELICITATION_STRICT_AUTO_REVIEW_KEY;
|
||||
use codex_protocol::mcp_approval_meta::TOOL_DESCRIPTION_KEY as MCP_ELICITATION_TOOL_DESCRIPTION_KEY;
|
||||
use codex_protocol::mcp_approval_meta::TOOL_NAME_KEY as MCP_ELICITATION_TOOL_NAME_KEY;
|
||||
use codex_protocol::mcp_approval_meta::TOOL_PARAMS_KEY as MCP_ELICITATION_TOOL_PARAMS_KEY;
|
||||
@@ -649,7 +652,7 @@ async fn review_guardian_mcp_elicitation(
|
||||
session: Arc<Session>,
|
||||
request: ElicitationReviewRequest,
|
||||
) -> anyhow::Result<Option<ElicitationResponse>> {
|
||||
let Some((turn_context, _cancellation_token)) =
|
||||
let Some((turn_context, cancellation_token)) =
|
||||
session.active_turn_context_and_cancellation_token().await
|
||||
else {
|
||||
return Ok(None);
|
||||
@@ -658,6 +661,100 @@ async fn review_guardian_mcp_elicitation(
|
||||
let Some(mcp_config) = session.services.mcp_runtime.current_config() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if matches!(
|
||||
request
|
||||
.elicitation
|
||||
.meta()
|
||||
.and_then(|meta| meta.get(MCP_ELICITATION_STRICT_AUTO_REVIEW_KEY)),
|
||||
Some(Value::Bool(true))
|
||||
) {
|
||||
let connector_id = elicitation_connector_id(&request.elicitation);
|
||||
let trusted_guardian_request = if request.server_name == CODEX_APPS_MCP_SERVER_NAME {
|
||||
let Some(call_id) = request
|
||||
.elicitation
|
||||
.meta()
|
||||
.and_then(|meta| meta.get(MCP_TOOL_CODEX_APPS_META_KEY))
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|meta| meta.get("call_id"))
|
||||
.and_then(Value::as_str)
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some((Some(invocation), metadata)) = session
|
||||
.mcp_tool_approval_metadata(&turn_context.sub_id, call_id)
|
||||
.await
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
if invocation.server != request.server_name
|
||||
|| connector_id != metadata.connector_id.as_deref()
|
||||
|| request
|
||||
.elicitation
|
||||
.meta()
|
||||
.and_then(|meta| metadata_str(meta, MCP_ELICITATION_TOOL_NAME_KEY))
|
||||
!= Some(invocation.tool.as_str())
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
Some(
|
||||
crate::mcp_tool_call::build_guardian_mcp_tool_review_request(
|
||||
call_id,
|
||||
&invocation,
|
||||
Some(&metadata),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if !turn_context
|
||||
.config
|
||||
.features
|
||||
.enabled(Feature::GuardianApproval)
|
||||
|| mcp_config
|
||||
.config_layer_stack
|
||||
.requirements()
|
||||
.approvals_reviewer
|
||||
.can_set(&ApprovalsReviewer::AutoReview)
|
||||
.is_err()
|
||||
|| crate::connectors::mcp_approvals_reviewer_from_layers(
|
||||
&mcp_config.config_layer_stack,
|
||||
ApprovalsReviewer::AutoReview,
|
||||
request.server_name.as_str(),
|
||||
connector_id,
|
||||
) != ApprovalsReviewer::AutoReview
|
||||
|| request
|
||||
.elicitation
|
||||
.meta()
|
||||
.is_some_and(|meta| meta.contains_key(MCP_ELICITATION_PERSIST_KEY))
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let GuardianElicitationReview::ApprovalRequest(guardian_request) =
|
||||
guardian_elicitation_review_request(&request)
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let guardian_request = trusted_guardian_request.unwrap_or(*guardian_request);
|
||||
let decision = crate::guardian::review_approval_request_with_cancel(
|
||||
&session,
|
||||
&turn_context,
|
||||
crate::guardian::new_guardian_review_id(),
|
||||
guardian_request,
|
||||
/*retry_reason*/ None,
|
||||
crate::guardian::GuardianReviewOptions {
|
||||
plugin_attribution_override: None,
|
||||
approval_request_source: codex_analytics::GuardianApprovalRequestSource::MainTurn,
|
||||
external_cancel: Some(cancellation_token),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
return Ok(matches!(decision, ReviewDecision::Approved)
|
||||
.then(|| mcp_elicitation_response_from_guardian_decision(decision)));
|
||||
}
|
||||
|
||||
let approval_policy = mcp_config.approval_policy.value();
|
||||
match approval_policy {
|
||||
AskForApproval::Never => {
|
||||
@@ -688,6 +785,7 @@ async fn review_guardian_mcp_elicitation(
|
||||
AskForApproval::OnRequest | AskForApproval::UnlessTrusted | AskForApproval::Granular(_) => {
|
||||
}
|
||||
}
|
||||
|
||||
let approvals_reviewer = crate::connectors::mcp_approvals_reviewer_from_layers(
|
||||
&mcp_config.config_layer_stack,
|
||||
mcp_config.approvals_reviewer,
|
||||
|
||||
@@ -23,6 +23,7 @@ use crate::session::TurnInputQueue;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::tasks::AnySessionTask;
|
||||
use codex_protocol::models::AdditionalPermissionProfile;
|
||||
use codex_protocol::protocol::McpInvocation;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
|
||||
@@ -88,7 +89,7 @@ pub(crate) struct TurnState {
|
||||
pending_request_permissions: HashMap<String, PendingRequestPermissions>,
|
||||
pending_user_input: HashMap<String, oneshot::Sender<RequestUserInputResponse>>,
|
||||
pending_elicitations: HashMap<(String, RequestId), oneshot::Sender<ElicitationResponse>>,
|
||||
mcp_tool_approval_metadata: HashMap<String, McpToolApprovalMetadata>,
|
||||
mcp_tool_approval_metadata: HashMap<String, (Option<McpInvocation>, McpToolApprovalMetadata)>,
|
||||
pending_dynamic_tools: HashMap<String, oneshot::Sender<DynamicToolResponse>>,
|
||||
pub(crate) pending_input: TurnInputQueue,
|
||||
mailbox_delivery_phase: MailboxDeliveryPhase,
|
||||
@@ -183,15 +184,17 @@ impl TurnState {
|
||||
pub(crate) fn insert_mcp_tool_approval_metadata(
|
||||
&mut self,
|
||||
call_id: String,
|
||||
invocation: Option<McpInvocation>,
|
||||
metadata: McpToolApprovalMetadata,
|
||||
) {
|
||||
self.mcp_tool_approval_metadata.insert(call_id, metadata);
|
||||
self.mcp_tool_approval_metadata
|
||||
.insert(call_id, (invocation, metadata));
|
||||
}
|
||||
|
||||
pub(crate) fn mcp_tool_approval_metadata(
|
||||
&self,
|
||||
call_id: &str,
|
||||
) -> Option<McpToolApprovalMetadata> {
|
||||
) -> Option<(Option<McpInvocation>, McpToolApprovalMetadata)> {
|
||||
self.mcp_tool_approval_metadata.get(call_id).cloned()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user