Add conservative restriction helpers for tool policies (#38183)

## What changed

- Add `restrict_to` helpers that combine parent and requested `WebSearchMode`
  and `AppToolApproval` values without granting access beyond either policy.
- Treat `Auto` and `Writes` approval modes as incomparable, falling back to
  `Prompt` when they are combined.

## Testing

- Cover every pair of web search and app tool approval modes.

GitOrigin-RevId: 41b3da5ace2fbdaab667c4b834bc2051a8e5e9cf
This commit is contained in:
jif
2026-08-12 12:53:07 +00:00
committed by copyberry
parent 0e82c62a44
commit 93beee910d
3 changed files with 80 additions and 0 deletions
+16
View File
@@ -30,6 +30,22 @@ pub enum AppToolApproval {
Approve,
}
impl AppToolApproval {
/// Requires approval whenever either policy could require it.
///
/// `Auto` and `Writes` are incomparable: each can require approval for a
/// tool the other would approve. Their conservative intersection is `Prompt`.
pub fn restrict_to(self, requested: Self) -> Self {
match (self, requested) {
(Self::Prompt, _) | (_, Self::Prompt) => Self::Prompt,
(Self::Approve, mode) | (mode, Self::Approve) => mode,
(Self::Auto, Self::Auto) => Self::Auto,
(Self::Writes, Self::Writes) => Self::Writes,
(Self::Auto, Self::Writes) | (Self::Writes, Self::Auto) => Self::Prompt,
}
}
}
/// Human-readable reason a configured MCP server was disabled after requirements
/// were applied.
///
+26
View File
@@ -4,6 +4,32 @@ use pretty_assertions::assert_eq;
use std::collections::HashMap;
use std::path::Path;
#[test]
fn app_tool_approval_restrictions_never_weaken_either_policy() {
use AppToolApproval::Approve;
use AppToolApproval::Auto;
use AppToolApproval::Prompt;
use AppToolApproval::Writes;
let modes = [Approve, Auto, Writes, Prompt];
let expected = [
[Approve, Auto, Writes, Prompt],
[Auto, Auto, Prompt, Prompt],
[Writes, Prompt, Writes, Prompt],
[Prompt, Prompt, Prompt, Prompt],
];
for (parent_index, parent) in modes.into_iter().enumerate() {
for (requested_index, requested) in modes.into_iter().enumerate() {
assert_eq!(
parent.restrict_to(requested),
expected[parent_index][requested_index],
"parent: {parent:?}, requested: {requested:?}",
);
}
}
}
#[test]
fn deserialize_stdio_command_server_config() {
let cfg: McpServerConfig = toml::from_str(
+38
View File
@@ -362,6 +362,18 @@ pub enum WebSearchMode {
Live,
}
impl WebSearchMode {
/// Restricts search to the access permitted by both modes.
pub fn restrict_to(self, requested: Self) -> Self {
match (self, requested) {
(Self::Disabled, _) | (_, Self::Disabled) => Self::Disabled,
(Self::Cached, _) | (_, Self::Cached) => Self::Cached,
(Self::Indexed, _) | (_, Self::Indexed) => Self::Indexed,
(Self::Live, Self::Live) => Self::Live,
}
}
}
/// A model-facing surface on which a tool can be exposed.
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
@@ -768,6 +780,32 @@ mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn web_search_mode_restrictions_never_expand_either_mode() {
use WebSearchMode::Cached;
use WebSearchMode::Disabled;
use WebSearchMode::Indexed;
use WebSearchMode::Live;
let modes = [Disabled, Cached, Indexed, Live];
let expected = [
[Disabled, Disabled, Disabled, Disabled],
[Disabled, Cached, Cached, Cached],
[Disabled, Cached, Indexed, Indexed],
[Disabled, Cached, Indexed, Live],
];
for (parent_index, parent) in modes.into_iter().enumerate() {
for (requested_index, requested) in modes.into_iter().enumerate() {
assert_eq!(
parent.restrict_to(requested),
expected[parent_index][requested_index],
"parent: {parent:?}, requested: {requested:?}",
);
}
}
}
#[test]
fn apply_mask_can_clear_optional_fields() {
let mode = CollaborationMode {