Honor app link settings for MCP tool approvals (#42056)

## What changed

- Pass the resolved app `link_id` into MCP tool approval policy evaluation.
- Apply link-specific `default_tools_approval_mode` and `approvals_reviewer`
  settings before app and global defaults, while preserving per-tool and managed
  policy precedence.

## Testing

- Cover link-specific approval prompts, selected account links, reviewer
  routing, fallback behavior, and managed-policy overrides.

GitOrigin-RevId: caa07ec3e73c87aad9522bd69808cd1c0b1686f5
This commit is contained in:
Alex Zamoshchin authored and copyberry committed 2026-09-01 13:43:27 +00:00
1 parent 0e37d834d4
commit 3a04482645
11 files changed
+429 -39

No files matched your search

+8 -1
View File
@@ -22,10 +22,11 @@ impl Default for AppToolPolicy {
}
}
/// Connector-owned metadata used to evaluate one app tool.
/// App and account metadata used to evaluate one tool.
#[derive(Debug, Clone, Copy)]
pub struct AppToolPolicyInput<'a> {
pub connector_id: Option<&'a str>,
pub link_id: Option<&'a str>,
pub tool_name: &'a str,
pub tool_title: Option<&'a str>,
pub destructive_hint: Option<bool>,
@@ -186,6 +187,12 @@ fn app_tool_policy_from_apps_config(
});
let approval = managed_approval
.or_else(|| tool_config.and_then(|tool| tool.approval_mode))
.or_else(|| {
input
.link_id
.and_then(|link_id| app?.links.as_ref()?.links.get(link_id))
.and_then(|link| link.default_tools_approval_mode)
})
.or_else(|| app.and_then(|app| app.default_tools_approval_mode))
.or_else(|| {
input
@@ -488,6 +488,112 @@ fn managed_approval_overrides_user_tool_approval() {
);
}
#[test]
fn link_privacy_mode_overrides_app_default_and_preserves_tool_settings() {
let apps_config = serde_json::from_value(serde_json::json!({
"calendar": {
"default_tools_approval_mode": "auto",
"tools": { "events/create": { "approval_mode": "writes" } },
"links": {
"link_calendar": { "default_tools_approval_mode": "approve" },
"link_other": { "default_tools_approval_mode": "prompt" },
"link_without_privacy": {},
},
},
"drive": {
"links": { "link_drive": { "default_tools_approval_mode": "prompt" } },
},
"without_links": {
"default_tools_approval_mode": "writes",
},
"empty_links": {
"default_tools_approval_mode": "prompt",
"links": {},
},
}))
.expect("apps config");
let evaluator = AppToolPolicyEvaluator::from_parts(
Some(apps_config),
/*requirements_apps_config*/ None,
);
for (link_id, approval) in [
(Some("link_calendar"), AppToolApproval::Approve),
(Some("link_other"), AppToolApproval::Prompt),
(Some("link_without_privacy"), AppToolApproval::Auto),
(Some("link_drive"), AppToolApproval::Auto),
(None, AppToolApproval::Auto),
] {
assert_eq!(
evaluator.policy(AppToolPolicyInput {
link_id,
..input("events/list", /*tool_title*/ None)
}),
AppToolPolicy {
enabled: true,
approval,
}
);
}
assert_eq!(
evaluator.policy(AppToolPolicyInput {
link_id: Some("link_calendar"),
..input("events/create", /*tool_title*/ None)
}),
AppToolPolicy {
enabled: true,
approval: AppToolApproval::Writes,
}
);
for (connector_id, approval) in [
("without_links", AppToolApproval::Writes),
("empty_links", AppToolApproval::Prompt),
] {
assert_eq!(
evaluator.policy(AppToolPolicyInput {
connector_id: Some(connector_id),
link_id: Some("link_calendar"),
..input("events/list", /*tool_title*/ None)
}),
AppToolPolicy {
enabled: true,
approval,
}
);
}
}
#[test]
fn link_privacy_mode_preserves_managed_connector_requirements() {
let apps_config = serde_json::from_value(serde_json::json!({
"calendar": {
"links": { "link_calendar": { "default_tools_approval_mode": "approve" } },
},
}))
.expect("apps config");
let mut requirements =
app_tool_requirements("calendar", "events/create", AppToolApproval::Prompt);
requirements
.apps
.get_mut("calendar")
.expect("calendar requirement")
.enabled = Some(false);
let evaluator = AppToolPolicyEvaluator::from_parts(Some(apps_config), Some(&requirements));
assert_eq!(
evaluator.policy(AppToolPolicyInput {
link_id: Some("link_calendar"),
..input("events/create", /*tool_title*/ None)
}),
AppToolPolicy {
enabled: false,
approval: AppToolApproval::Prompt,
}
);
}
#[test]
fn per_tool_enable_overrides_app_level_hints() {
let apps_config = AppsConfigToml {
@@ -705,6 +811,7 @@ fn evaluator_matches_tool_title_for_user_config() {
fn input<'a>(tool_name: &'a str, tool_title: Option<&'a str>) -> AppToolPolicyInput<'a> {
AppToolPolicyInput {
connector_id: Some("calendar"),
link_id: None,
tool_name,
tool_title,
destructive_hint: Some(true),
@@ -795,6 +902,7 @@ fn policy_from_config_parts(
};
AppToolPolicyEvaluator::new(&config_layer_stack).policy(AppToolPolicyInput {
connector_id,
link_id: None,
tool_name,
tool_title,
destructive_hint,
@@ -69,6 +69,7 @@ pub fn installed_connector_runtime<'a>(
let policy_allows_tool = policy
.policy(AppToolPolicyInput {
connector_id: Some(connector_id),
link_id: None,
tool_name: tool.tool_name,
tool_title: tool.tool_title,
destructive_hint: tool.destructive_hint,