Honor explicit account selectors for Apps tool calls (#42054)

## What changed

- Resolve `link_id` from tool-call arguments when Apps metadata sets
  `requires_explicit_link_id` to `true`.
- Reject the call before approval or execution when the required selector is
  missing, empty, or not a string.
- Preserve catalog-provided account metadata for legacy Apps tools and leave
  non-Apps MCP tools unchanged.

## Testing

- Add unit coverage for required selectors, malformed values, legacy
  fallbacks, and non-Apps tools.
- Add end-to-end coverage for execution, approval prompts, and rejection when
  an Apps call omits `link_id`.

GitOrigin-RevId: 667fd04102a3934d53ded020da6d1bf68a1ca3e5
This commit is contained in:
Alex Zamoshchin authored and copyberry committed 2026-09-01 13:29:18 +00:00
1 parent 0ec375eb70
commit 0e37d834d4
4 files changed
+365 -16

No files matched your search

+44 -14
View File
@@ -96,8 +96,10 @@ use tracing::error;
use tracing::field::Empty;
use url::Url;
mod account;
mod telemetry;
use account::McpToolAccountError;
use telemetry::McpCallMetricOutcome;
use telemetry::emit_mcp_call_metrics;
use telemetry::mcp_call_metric_outcome;
@@ -173,7 +175,32 @@ pub(crate) async fn handle_mcp_tool_call(
.unwrap_or_else(|| JsonValue::Object(serde_json::Map::new())),
};
};
let metadata = mcp_tool_metadata(&prepared_call);
let metadata = match mcp_tool_metadata(
prepared_call.tool_info(),
prepared_call.plugin_id(),
invocation.arguments.as_ref(),
) {
Ok(metadata) => metadata,
Err(err) => {
let item_metadata =
McpToolCallItemMetadata::from_tool_metadata(&server, /*metadata*/ None);
let result = notify_mcp_tool_call_skip(
sess.as_ref(),
turn_context.as_ref(),
&call_id,
invocation,
item_metadata,
err.to_string(),
/*already_started*/ false,
)
.await;
return HandledMcpToolCall {
result: CallToolResult::from_result(result),
tool_input: arguments_value
.unwrap_or_else(|| JsonValue::Object(serde_json::Map::new())),
};
}
};
let item_metadata = McpToolCallItemMetadata::from_tool_metadata(&server, Some(&metadata));
let runtime_config = prepared_call.config();
let app_tool_policy = if server == CODEX_APPS_MCP_SERVER_NAME {
@@ -1642,9 +1669,13 @@ pub(crate) fn build_guardian_mcp_tool_review_request(
}
}
fn mcp_tool_metadata(prepared_call: &PreparedMcpCall) -> McpToolApprovalMetadata {
let server = prepared_call.server_name();
let tool_info = prepared_call.tool_info().clone();
fn mcp_tool_metadata(
tool_info: &ToolInfo,
plugin_id: Option<&str>,
arguments: Option<&JsonValue>,
) -> Result<McpToolApprovalMetadata, McpToolAccountError> {
let server = tool_info.server_name.as_str();
let tool_info = tool_info.clone();
let connector_description = (server == CODEX_APPS_MCP_SERVER_NAME)
.then(|| tool_info.namespace_description.clone())
.flatten();
@@ -1656,6 +1687,11 @@ fn mcp_tool_metadata(prepared_call: &PreparedMcpCall) -> McpToolApprovalMetadata
.and_then(|meta| meta.get(MCP_TOOL_CODEX_APPS_META_KEY))
.and_then(serde_json::Value::as_object)
.cloned();
let link_id = if server == CODEX_APPS_MCP_SERVER_NAME {
account::resolve_account(&tool_info, arguments)?
} else {
None
};
let connected_account_email = if server == CODEX_APPS_MCP_SERVER_NAME {
codex_apps_meta
.as_ref()
@@ -1668,20 +1704,14 @@ fn mcp_tool_metadata(prepared_call: &PreparedMcpCall) -> McpToolApprovalMetadata
None
};
McpToolApprovalMetadata {
Ok(McpToolApprovalMetadata {
annotations: tool_info.tool.annotations,
connector_id: tool_info.connector_id,
link_id: tool_info
.tool
.meta
.as_ref()
.and_then(|meta| meta.get(MCP_TOOL_LINK_ID_META_KEY))
.and_then(serde_json::Value::as_str)
.map(str::to_string),
link_id,
connector_name: tool_info.connector_name,
connector_description,
connected_account_email,
plugin_id: prepared_call.plugin_id().map(str::to_string),
plugin_id: plugin_id.map(str::to_string),
tool_title: tool_info.tool.title,
tool_description: tool_info.tool.description.map(std::borrow::Cow::into_owned),
mcp_app_resource_uri: get_mcp_app_resource_uri(tool_info.tool.meta.as_deref()),
@@ -1691,7 +1721,7 @@ fn mcp_tool_metadata(prepared_call: &PreparedMcpCall) -> McpToolApprovalMetadata
server,
&tool_info.openai_file_input_optional_fields,
),
}
})
}
fn openai_file_input_optional_fields_for_server(