Route MCP elicitations through the shared approval decision path (#43447)

## What changed

- Use `decide_approval` for MCP elicitations, carrying the effective approval policy, reviewer, and synchronous-review requirement into the decision.
- Preserve unsupported form and URL elicitations for user review.
- Use `computer_use_review_required()` for review evidence, session policy, and turn metadata so model policy takes precedence over the legacy flag.
- Derive strict-review notifications from each assessment's review reason.

## Testing

Add coverage for model policy precedence, user and full-access modes, sensitive elicitations, independent code-mode and nested-tool policies, cached score reuse, and user review of unsupported elicitations.

GitOrigin-RevId: d0a9c40572dca7b3dbf876230d2162b72728084d
This commit is contained in:
jif authored and copyberry committed 2026-09-07 12:04:27 +00:00
1 parent c0b6285711
commit ce5c4133bd
12 files changed
+585 -129

No files matched your search

+95 -101
View File
@@ -807,13 +807,15 @@ async fn review_guardian_mcp_elicitation(
Some(Value::Bool(true))
);
if matches!(
let mut review_context = crate::guardian::GuardianReviewContext::from(&turn_context);
let strict_auto_review = matches!(
request
.elicitation
.meta()
.and_then(|meta| meta.get(MCP_ELICITATION_STRICT_AUTO_REVIEW_KEY)),
Some(Value::Bool(true))
) {
);
let guardian_request = if strict_auto_review {
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
@@ -883,109 +885,96 @@ async fn review_guardian_mcp_elicitation(
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 {
require_guardian: false,
plugin_attribution_override: None,
approval_request_source: codex_analytics::GuardianApprovalRequestSource::MainTurn,
external_cancel: Some(cancellation_token),
require_synchronous_review,
},
)
.await;
return Ok(matches!(
decision,
ReviewDecision::Approved
| ReviewDecision::Denied { .. }
| ReviewDecision::TimedOut
| ReviewDecision::Abort
)
.then(|| {
mcp_elicitation_response_from_guardian_decision(decision, turn_context.model_info())
}));
}
let approval_policy = mcp_config.approval_policy.value();
match approval_policy {
AskForApproval::Never => {
let Some(permission_profile) =
mcp_config.permission_profile_for_server(&request.server_name)
else {
trusted_guardian_request.unwrap_or(*guardian_request).into()
} else {
let approval_policy = mcp_config.approval_policy.value();
match approval_policy {
AskForApproval::Never => {
let Some(permission_profile) =
mcp_config.permission_profile_for_server(&request.server_name)
else {
return Ok(Some(mcp_elicitation_decline_without_message()));
};
if codex_mcp::mcp_permission_prompt_is_auto_approved(
approval_policy,
permission_profile,
codex_mcp::McpPermissionPromptAutoApproveContext::default(),
) && matches!(
&request.elicitation,
Elicitation::Mcp(
rmcp::model::ElicitRequestParams::FormElicitationParams {
requested_schema,
..
}
) if requested_schema.properties.is_empty()
) {
return Ok(Some(ElicitationResponse {
action: ElicitationAction::Accept,
content: Some(serde_json::json!({})),
meta: None,
}));
}
return Ok(Some(mcp_elicitation_decline_without_message()));
};
if codex_mcp::mcp_permission_prompt_is_auto_approved(
approval_policy,
permission_profile,
codex_mcp::McpPermissionPromptAutoApproveContext::default(),
) && matches!(
&request.elicitation,
Elicitation::Mcp(
rmcp::model::ElicitRequestParams::FormElicitationParams {
requested_schema,
..
}
) if requested_schema.properties.is_empty()
) {
return Ok(Some(ElicitationResponse {
action: ElicitationAction::Accept,
content: Some(serde_json::json!({})),
meta: None,
}));
}
return Ok(Some(mcp_elicitation_decline_without_message()));
AskForApproval::Granular(config) if !config.allows_mcp_elicitations() => {
return Ok(Some(mcp_elicitation_decline_without_message()));
}
AskForApproval::OnRequest
| AskForApproval::UnlessTrusted
| AskForApproval::Granular(_) => {}
}
AskForApproval::Granular(config) if !config.allows_mcp_elicitations() => {
return Ok(Some(mcp_elicitation_decline_without_message()));
}
AskForApproval::OnRequest | AskForApproval::UnlessTrusted | AskForApproval::Granular(_) => {
}
}
let approvals_reviewer = crate::connectors::mcp_approvals_reviewer_from_layers(
&mcp_config.config_layer_stack,
step_settings
.mcp_approvals_reviewer_override
.unwrap_or(mcp_config.approvals_reviewer),
Some(turn_context.model_info().slug.as_str()),
request.server_name.as_str(),
elicitation_connector_id(&request.elicitation),
/*link_id*/ None,
);
if !crate::guardian::routes_approval_policy_to_guardian(approval_policy, approvals_reviewer) {
return Ok(None);
}
let guardian_request = match guardian_elicitation_review_request(&request, originating_call_id)
{
GuardianElicitationReview::NotRequested => return Ok(None),
GuardianElicitationReview::Decline(reason) => {
warn!(
server_name = %request.server_name,
request_id = %mcp_elicitation_request_id(&request.request_id),
reason,
"declining Guardian MCP elicitation before review"
);
return Ok(Some(mcp_elicitation_decline_without_message()));
let approvals_reviewer = crate::connectors::mcp_approvals_reviewer_from_layers(
&mcp_config.config_layer_stack,
step_settings
.mcp_approvals_reviewer_override
.unwrap_or(mcp_config.approvals_reviewer),
Some(turn_context.model_info().slug.as_str()),
request.server_name.as_str(),
elicitation_connector_id(&request.elicitation),
/*link_id*/ None,
);
review_context.approval_policy = approval_policy;
review_context.approvals_reviewer = approvals_reviewer;
match guardian_elicitation_review_request(&request, originating_call_id) {
GuardianElicitationReview::NotRequested => return Ok(None),
GuardianElicitationReview::Decline(reason) => {
let Elicitation::Mcp(elicitation) = &request.elicitation else {
return Ok(None);
};
crate::guardian::ReviewAction {
action: serde_json::to_value(elicitation)
.map(|elicitation| {
serde_json::json!({
"tool": "mcp_elicitation",
"server": request.server_name,
"request": elicitation,
})
})
.map_err(|error| error.to_string()),
category: codex_protocol::openai_models::GuardianScope::for_mcp_server(
&request.server_name,
),
request: Err(reason.to_owned()),
}
}
GuardianElicitationReview::ApprovalRequest(guardian_request) => {
(*guardian_request).into()
}
}
GuardianElicitationReview::ApprovalRequest(guardian_request) => *guardian_request,
};
let review_id = crate::guardian::new_guardian_review_id();
let decision = crate::guardian::review_approval_request_with_cancel(
&session,
&turn_context,
review_id,
let declined_reason = guardian_request.request.as_ref().err().cloned();
let decision = crate::guardian::decide_approval(
session,
review_context,
crate::guardian::new_guardian_review_id(),
guardian_request,
/*retry_reason*/ None,
crate::tools::sandboxing::ApprovalRequestReasons {
approval: None,
retry: None,
},
crate::guardian::GuardianReviewOptions {
require_guardian: false,
require_guardian: strict_auto_review,
plugin_attribution_override: None,
approval_request_source: codex_analytics::GuardianApprovalRequestSource::MainTurn,
external_cancel: Some(cancellation_token),
@@ -993,10 +982,15 @@ async fn review_guardian_mcp_elicitation(
},
)
.await;
Ok(Some(mcp_elicitation_response_from_guardian_decision(
decision,
turn_context.model_info(),
)))
Ok(decision.map(|decision| {
if let Some(reason) = declined_reason {
warn!(server_name = %request.server_name,
request_id = %mcp_elicitation_request_id(&request.request_id), reason,
"declining Guardian MCP elicitation before review");
return mcp_elicitation_decline_without_message();
}
mcp_elicitation_response_from_guardian_decision(decision, turn_context.model_info())
}))
}
fn guardian_elicitation_review_request(