mirror of
https://github.com/openai/codex.git
synced 2026-11-08 20:40:33 +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:
@@ -32,7 +32,7 @@ pub enum NodeReplReviewEvidenceMode {
|
||||
|
||||
pub(crate) fn node_repl_review_evidence_mode(turn: &TurnContext) -> NodeReplReviewEvidenceMode {
|
||||
let features = &turn.config.features;
|
||||
if turn.model_info().node_repl_auto_review_required
|
||||
if turn.model_info().computer_use_review_required()
|
||||
|| features.enabled(Feature::GuardianEnhancedNodeReplTranscripts)
|
||||
&& features.enabled(Feature::GuardianNodeReplTranscriptImages)
|
||||
{
|
||||
|
||||
@@ -48,6 +48,7 @@ pub(crate) use review::new_guardian_review_id;
|
||||
pub(crate) use review::record_guardian_denial_for_test;
|
||||
#[cfg(test)]
|
||||
pub(crate) use review::review_approval_request;
|
||||
#[cfg(test)]
|
||||
pub(crate) use review::review_approval_request_with_cancel;
|
||||
pub(crate) use review::routes_approval_policy_to_guardian;
|
||||
pub(crate) use review::routes_approval_to_guardian;
|
||||
|
||||
@@ -5,9 +5,12 @@ use codex_analytics::GuardianReviewFailureReason;
|
||||
use codex_analytics::GuardianReviewTerminalStatus;
|
||||
use codex_analytics::GuardianReviewTrackContext;
|
||||
use codex_analytics::GuardianReviewedAction;
|
||||
#[cfg(test)]
|
||||
use codex_analytics::GuardianV2Event;
|
||||
#[cfg(test)]
|
||||
use codex_analytics::GuardianV2EventKind;
|
||||
use codex_core_plugins::PluginCommandAttribution;
|
||||
#[cfg(test)]
|
||||
use codex_extension_api::GuardianV2Enabled;
|
||||
use codex_extension_api::ThreadIdleCause;
|
||||
use codex_features::Feature;
|
||||
@@ -53,6 +56,7 @@ use super::GuardianAssessmentOutcome;
|
||||
use super::GuardianRejectionCircuitBreakerAction;
|
||||
use super::GuardianRejectionCircuitBreakerPolicy;
|
||||
use super::GuardianReviewContext;
|
||||
#[cfg(test)]
|
||||
use super::approval_request::format_guardian_action_compact;
|
||||
use super::approval_request::format_guardian_action_pretty;
|
||||
use super::approval_request::guardian_assessment_action;
|
||||
@@ -340,6 +344,7 @@ pub(crate) async fn record_guardian_denial_for_test(
|
||||
/// Runs Guardian unless Full Access or an installed extension resolves the review.
|
||||
/// Guardian timeouts, review-session failures, and parse failures all block
|
||||
/// execution, with timeouts surfaced separately from explicit denials.
|
||||
#[cfg(test)]
|
||||
async fn run_guardian_review(
|
||||
session: Arc<Session>,
|
||||
context: GuardianReviewContext,
|
||||
@@ -927,6 +932,7 @@ pub(crate) async fn review_approval_request(
|
||||
review.await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn review_approval_request_with_cancel(
|
||||
session: &Arc<Session>,
|
||||
context: impl Into<GuardianReviewContext>,
|
||||
@@ -1038,7 +1044,7 @@ pub(super) async fn guardian_review_session_config(
|
||||
guardian_reasoning_effort.clone(),
|
||||
guardian_model_info.model_messages.as_ref(),
|
||||
)?;
|
||||
if turn.model_info().node_repl_auto_review_required {
|
||||
if turn.model_info().computer_use_review_required() {
|
||||
spawn_config
|
||||
.features
|
||||
.enable(Feature::RetainClientDeveloperMessages)
|
||||
|
||||
@@ -500,7 +500,7 @@ impl GuardianReviewSessionManager {
|
||||
parent_context
|
||||
.turn()
|
||||
.model_info()
|
||||
.node_repl_auto_review_required,
|
||||
.computer_use_review_required(),
|
||||
)
|
||||
.with_node_repl_policy(&session_config.node_repl_policy);
|
||||
reuse_key.root_authorization_version = root_authorization_version;
|
||||
@@ -613,7 +613,7 @@ impl GuardianReviewSessionManager {
|
||||
.parent_context
|
||||
.turn()
|
||||
.model_info()
|
||||
.node_repl_auto_review_required,
|
||||
.computer_use_review_required(),
|
||||
)
|
||||
.with_node_repl_policy(¶ms.node_repl_policy);
|
||||
next_reuse_key.root_authorization_version = root_authorization_version;
|
||||
@@ -1424,7 +1424,7 @@ async fn ensure_guardian_node_repl_policy(
|
||||
.parent_context
|
||||
.turn()
|
||||
.model_info()
|
||||
.node_repl_auto_review_required
|
||||
.computer_use_review_required()
|
||||
|| !matches!(
|
||||
¶ms.request,
|
||||
GuardianApprovalRequest::McpToolCall { server, tool_name, .. }
|
||||
|
||||
@@ -17,9 +17,9 @@ use crate::session::session::Session;
|
||||
/// This lets the extension return AskUser before invoking that renderer.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ReviewAction {
|
||||
pub(super) action: Result<serde_json::Value, String>,
|
||||
pub(super) category: codex_protocol::openai_models::GuardianScope,
|
||||
pub(super) request: Result<GuardianApprovalRequest, String>,
|
||||
pub(crate) action: Result<serde_json::Value, String>,
|
||||
pub(crate) category: codex_protocol::openai_models::GuardianScope,
|
||||
pub(crate) request: Result<GuardianApprovalRequest, String>,
|
||||
}
|
||||
|
||||
impl From<GuardianApprovalRequest> for ReviewAction {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -218,7 +218,7 @@ impl TurnMetadataState {
|
||||
turn_id,
|
||||
sandbox_tags,
|
||||
auto_review_enabled,
|
||||
node_repl_auto_review_required: model_info.node_repl_auto_review_required,
|
||||
node_repl_auto_review_required: model_info.computer_use_review_required(),
|
||||
node_repl_disabled: model_info.node_repl_disabled,
|
||||
enriched_workspaces: RwLock::new(None),
|
||||
tool_namespaces_info: RwLock::new(None),
|
||||
|
||||
Reference in New Issue
Block a user