Attribute nested REPL reviews to their tool calls (#42082)

## What changed

- Retain MCP invocation metadata for Node-backed REPL servers so nested
  elicitations can resolve the enclosing tool call.
- Use a valid, same-server `callId` from the current turn as the Guardian
  assessment target while continuing to review each nested action and connector
  independently.
- Fall back to an elicitation-specific target when `callId` is missing,
  malformed, unknown, from another server, or from an earlier turn.

## Testing

- Cover ordinary and strict reviews for `node_repl` and `cua_repl`, multiple
  nested decisions, unrelated servers, and invalid or stale call IDs.

GitOrigin-RevId: adea4b5cff96726a540ff3b3ea902b0f5dd99299
This commit is contained in:
Ankush Gupta authored and copyberry committed 2026-09-01 15:34:03 +00:00
1 parent 2350823caa
commit 67cc3c318d
4 files changed
+366 -24

No files matched your search

+30 -7
View File
@@ -13,6 +13,7 @@ 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;
use codex_protocol::mcp::is_node_repl_backed_server;
use codex_protocol::mcp_approval_meta::APPROVAL_KIND_KEY as MCP_ELICITATION_APPROVAL_KIND_KEY;
use codex_protocol::mcp_approval_meta::APPROVAL_KIND_MCP_TOOL_CALL as MCP_ELICITATION_APPROVAL_KIND_MCP_TOOL_CALL;
use codex_protocol::mcp_approval_meta::APPROVAL_KIND_TOOL_SUGGESTION as MCP_ELICITATION_APPROVAL_KIND_TOOL_SUGGESTION;
@@ -724,6 +725,24 @@ async fn review_guardian_mcp_elicitation(
return Ok(None);
};
// The invocation identifies the tool event, but a nested elicitation can
// review a different action and connector than the enclosing JavaScript.
let originating_call_id = if is_node_repl_backed_server(&request.server_name)
&& let Some(call_id) = request
.elicitation
.meta()
.and_then(|meta| meta.get("callId"))
.and_then(Value::as_str)
&& let Some((Some(invocation), _)) = session
.mcp_tool_approval_metadata(&turn_context.sub_id, call_id)
.await
&& invocation.server == request.server_name
{
Some(call_id)
} else {
None
};
let require_synchronous_review = matches!(
request
.elicitation
@@ -804,7 +823,7 @@ async fn review_guardian_mcp_elicitation(
}
let GuardianElicitationReview::ApprovalRequest(guardian_request) =
guardian_elicitation_review_request(&request)
guardian_elicitation_review_request(&request, originating_call_id)
else {
return Ok(None);
};
@@ -884,7 +903,8 @@ async fn review_guardian_mcp_elicitation(
return Ok(None);
}
let guardian_request = match guardian_elicitation_review_request(&request) {
let guardian_request = match guardian_elicitation_review_request(&request, originating_call_id)
{
GuardianElicitationReview::NotRequested => return Ok(None),
GuardianElicitationReview::Decline(reason) => {
warn!(
@@ -921,6 +941,7 @@ async fn review_guardian_mcp_elicitation(
fn guardian_elicitation_review_request(
request: &ElicitationReviewRequest,
originating_call_id: Option<&str>,
) -> GuardianElicitationReview {
let (meta, requested_schema) = match &request.elicitation {
Elicitation::Mcp(rmcp::model::ElicitRequestParams::FormElicitationParams {
@@ -987,11 +1008,13 @@ fn guardian_elicitation_review_request(
GuardianElicitationReview::ApprovalRequest(Box::new(
crate::guardian::GuardianApprovalRequest::McpToolCall {
id: format!(
"mcp_elicitation:{}:{}",
request.server_name,
mcp_elicitation_request_id(&request.request_id)
),
id: originating_call_id.map(str::to_owned).unwrap_or_else(|| {
format!(
"mcp_elicitation:{}:{}",
request.server_name,
mcp_elicitation_request_id(&request.request_id)
)
}),
server: request.server_name.clone(),
tool_name,
arguments,
+12 -6
View File
@@ -47,7 +47,7 @@ fn guardian_elicitation_review_request_builds_mcp_tool_call() {
}))));
let GuardianElicitationReview::ApprovalRequest(guardian_request) =
guardian_elicitation_review_request(&request)
guardian_elicitation_review_request(&request, /*originating_call_id*/ None)
else {
panic!("expected Guardian MCP tool call request");
};
@@ -86,7 +86,7 @@ fn guardian_elicitation_review_request_defaults_missing_tool_params() {
let request = form_request(guardian_meta(/*tool_params*/ None));
let GuardianElicitationReview::ApprovalRequest(guardian_request) =
guardian_elicitation_review_request(&request)
guardian_elicitation_review_request(&request, /*originating_call_id*/ None)
else {
panic!("expected Guardian MCP tool call request");
};
@@ -163,7 +163,7 @@ fn guardian_elicitation_review_request_requires_opt_in() {
})));
assert_eq!(
guardian_elicitation_review_request(&request),
guardian_elicitation_review_request(&request, /*originating_call_id*/ None),
GuardianElicitationReview::NotRequested
);
}
@@ -181,7 +181,7 @@ fn guardian_elicitation_review_request_declines_unsupported_opt_in_shapes() {
}),
};
assert!(matches!(
guardian_elicitation_review_request(&url_request),
guardian_elicitation_review_request(&url_request, /*originating_call_id*/ None),
GuardianElicitationReview::Decline(_)
));
@@ -201,7 +201,10 @@ fn guardian_elicitation_review_request_declines_unsupported_opt_in_shapes() {
}),
};
assert!(matches!(
guardian_elicitation_review_request(&non_empty_schema_request),
guardian_elicitation_review_request(
&non_empty_schema_request,
/*originating_call_id*/ None
),
GuardianElicitationReview::Decline(_)
));
@@ -210,7 +213,10 @@ fn guardian_elicitation_review_request_declines_unsupported_opt_in_shapes() {
"codex_request_type": "approval_request",
})));
assert!(matches!(
guardian_elicitation_review_request(&missing_tool_name_request),
guardian_elicitation_review_request(
&missing_tool_name_request,
/*originating_call_id*/ None
),
GuardianElicitationReview::Decline(_)
));
}