mirror of
https://github.com/openai/codex.git
synced 2026-11-15 06:51:36 +00:00
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:
1 parent
c0b6285711
commit
ce5c4133bd
12 files changed
+585
-129
No files matched your search
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user