Use issuing-step context for Guardian MCP elicitation reviews (#51067)

## Why

MCP elicitation reviews used turn-start context, which could omit remote environments that became ready before the tool invocation and use stale environment permissions.

## What changed

Capture the issuing step's Guardian review context with each live MCP invocation and reuse it for elicitation reviews. Fall back to turn context when no matching live invocation is available, and preserve the captured context for in-flight actions.

## Testing

Extend the deferred-executor regression test to cover MCP elicitations, checking that Guardian receives the newly ready remote environment and its denied paths, excludes stale local denied paths, and returns the denial to the parent model.

GitOrigin-RevId: 0cc58b8f30543a9af87d53e1a90f574a8e6a2f84
This commit is contained in:
jif
2026-10-05 10:08:59 +00:00
committed by copyberry
parent 315f0efb34
commit 39e013c8b7
7 changed files with 112 additions and 53 deletions
+21 -8
View File
@@ -255,8 +255,12 @@ pub(crate) async fn handle_mcp_tool_call(
.unwrap_or_else(|| JsonValue::Object(serde_json::Map::new())),
};
}
let _approval_metadata =
sess.register_mcp_tool_approval_metadata(&call_id, &invocation, metadata.clone());
let _approval_metadata = sess.register_mcp_tool_approval_metadata(
&call_id,
&invocation,
metadata.clone(),
GuardianReviewContext::from(step_context),
);
notify_mcp_tool_call_started(
sess.as_ref(),
turn_context.as_ref(),
@@ -1189,20 +1193,29 @@ pub(crate) struct McpToolApprovalMetadata {
openai_file_input_optional_fields: Option<HashMap<String, Vec<String>>>,
}
#[derive(Clone)]
pub(crate) struct McpToolApprovalContext {
pub(crate) invocation: Option<McpInvocation>,
pub(crate) metadata: McpToolApprovalMetadata,
pub(crate) review_context: GuardianReviewContext,
}
impl Session {
fn register_mcp_tool_approval_metadata(
&self,
call_id: &str,
invocation: &McpInvocation,
metadata: McpToolApprovalMetadata,
) -> Arc<(Option<McpInvocation>, McpToolApprovalMetadata)> {
review_context: GuardianReviewContext,
) -> Arc<McpToolApprovalContext> {
let key = (invocation.server.clone(), call_id.to_string());
let metadata = Arc::new((
(invocation.server == CODEX_APPS_MCP_SERVER_NAME
let metadata = Arc::new(McpToolApprovalContext {
invocation: (invocation.server == CODEX_APPS_MCP_SERVER_NAME
|| is_node_repl_backed_server(&invocation.server))
.then(|| invocation.clone()),
metadata,
));
review_context,
});
let mut registry = self
.mcp_tool_approval_metadata
.lock()
@@ -1218,7 +1231,7 @@ impl Session {
&self,
server: &str,
call_id: &str,
) -> Option<(Option<McpInvocation>, McpToolApprovalMetadata)> {
) -> Option<McpToolApprovalContext> {
self.mcp_tool_approval_metadata
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
@@ -1627,7 +1640,7 @@ pub(crate) async fn request_mcp_tool_user_approval(
let (request_dispatched, decision) = if tool_call_mcp_elicitation_enabled {
let link_id = sess
.mcp_tool_approval_metadata(server, id)
.and_then(|(_, metadata)| metadata.link_id);
.and_then(|context| context.metadata.link_id);
let metadata = McpToolApprovalMetadata {
annotations: None,
connector_id: connector_id.clone(),