use super::RiskTier;
const BROWSER_PREFIX: &str = "browser_";
const BROWSER_READ_TOOLS: &[&str] = &[
"browser_snapshot",
"browser_take_screenshot",
"browser_screenshot",
"browser_console_messages",
"browser_network_requests",
"browser_generate_locator",
"browser_wait_for",
];
const BROWSER_READ_PREFIXES: &[&str] = &["browser_verify_"];
const BROWSER_PRIVILEGED_TOOLS: &[&str] = &["browser_storage_state", "browser_set_storage_state"];
fn classify_browser(leaf: &str) -> Option<RiskTier> {
if !leaf.starts_with(BROWSER_PREFIX) {
return None;
}
if BROWSER_PRIVILEGED_TOOLS.contains(&leaf) {
return Some(RiskTier::Privileged);
}
if BROWSER_READ_TOOLS.contains(&leaf)
|| BROWSER_READ_PREFIXES.iter().any(|p| leaf.starts_with(p))
{
return Some(RiskTier::Read);
}
Some(RiskTier::NetworkEgress)
}
pub fn browser_tier(tool_name: &str) -> Option<RiskTier> {
classify_browser(tool_name.rsplit("__").next().unwrap_or(tool_name))
}
const BROWSER_SERVER_HINTS: &[&str] = &["browser", "playwright"];
pub fn glob_may_cover_floor(prefix: &str) -> bool {
if prefix.is_empty() || prefix == "mcp__" {
return true;
}
let leaf = match prefix.strip_prefix("mcp__") {
None => prefix,
Some(rest) => match rest.split_once("__") {
Some((_, leaf)) if !leaf.is_empty() => leaf,
other => {
let server = other.map_or(rest, |(s, _)| s).to_ascii_lowercase();
return BROWSER_SERVER_HINTS
.iter()
.any(|h| server.contains(h) || h.starts_with(server.as_str()));
}
},
};
BROWSER_PREFIX.starts_with(leaf)
|| (leaf.starts_with(BROWSER_PREFIX)
&& !BROWSER_READ_PREFIXES.iter().any(|p| leaf.starts_with(p)))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FloorNote {
Waived { tier: RiskTier },
AsksForTool { tier: RiskTier },
AsksForSome,
IgnoredBypass,
BashCeiling { ceiling: RiskTier },
BashTrueIgnored,
}
pub fn floor_note(rule: &crate::agent::ToolRule) -> Option<FloorNote> {
use crate::agent::ToolPolicy;
if rule.policy != ToolPolicy::Allow || rule.risk.is_some_and(|t| !super::tier_may_be_granted(t))
{
return None;
}
if rule.pattern == super::floor_waiver::BASH_TOOL {
use super::floor_waiver::{FloorWaiver, bash_ceiling};
return Some(match rule.bypass_builtin_tier_floor {
FloorWaiver::All => FloorNote::BashTrueIgnored,
_ => FloorNote::BashCeiling {
ceiling: bash_ceiling(Some(rule)),
},
});
}
if let Some(prefix) = rule.pattern.strip_suffix('*') {
if !rule.bypass_builtin_tier_floor.is_off() {
return Some(FloorNote::IgnoredBypass);
}
return glob_may_cover_floor(prefix).then_some(FloorNote::AsksForSome);
}
let tier = browser_tier(&rule.pattern).filter(|t| !super::tier_may_be_granted(*t))?;
Some(if rule.bypass_builtin_tier_floor.covers(tier) {
FloorNote::Waived { tier }
} else {
FloorNote::AsksForTool { tier }
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tiers_by_leaf() {
assert_eq!(
browser_tier("mcp__browser__browser_click"),
Some(RiskTier::NetworkEgress)
);
assert_eq!(
browser_tier("mcp__x__browser_snapshot"),
Some(RiskTier::Read)
);
assert_eq!(browser_tier("browser_verify_value"), Some(RiskTier::Read));
assert_eq!(
browser_tier("mcp__b__browser_storage_state"),
Some(RiskTier::Privileged)
);
assert_eq!(browser_tier("bash"), None);
assert_eq!(browser_tier("mcp__browser__navigate"), None);
}
#[test]
fn glob_coverage_flags_catch_alls_and_browser_servers_only() {
for p in [
"",
"mcp__",
"mcp__browser__",
"mcp__playwright__",
"mcp__x__browser_",
"mcp__x__b",
"browser_",
"mcp__play",
] {
assert!(glob_may_cover_floor(p), "{p:?}");
}
for p in [
"mcp__github__",
"bash",
"mcp__x__browser_verify_",
"mcp__x__read_",
"edit_",
] {
assert!(!glob_may_cover_floor(p), "{p:?}");
}
}
#[test]
fn floor_notes_cover_waiver_bare_glob_and_ignored_bypass() {
use crate::agent::{ToolPolicy, ToolRule};
let r = |pattern: &str, policy, risk, bypass| ToolRule {
pattern: pattern.into(),
policy,
risk,
bypass_builtin_tier_floor: if bypass {
crate::hitl::floor_waiver::FloorWaiver::All
} else {
crate::hitl::floor_waiver::FloorWaiver::Off
},
};
let nav = "mcp__browser__browser_navigate";
let egress = RiskTier::NetworkEgress;
assert_eq!(
floor_note(&r(nav, ToolPolicy::Allow, None, true)),
Some(FloorNote::Waived { tier: egress })
);
assert_eq!(
floor_note(&r(nav, ToolPolicy::Allow, None, false)),
Some(FloorNote::AsksForTool { tier: egress })
);
assert_eq!(
floor_note(&r("mcp__browser__*", ToolPolicy::Allow, None, false)),
Some(FloorNote::AsksForSome)
);
assert_eq!(
floor_note(&r("mcp__browser__*", ToolPolicy::Allow, None, true)),
Some(FloorNote::IgnoredBypass)
);
for quiet in [
r("mcp__github__*", ToolPolicy::Allow, None, false),
r(
"mcp__browser__browser_snapshot",
ToolPolicy::Allow,
None,
false,
),
r(nav, ToolPolicy::Deny, None, false),
r(nav, ToolPolicy::Allow, Some(RiskTier::Destructive), true),
] {
assert_eq!(floor_note(&quiet), None, "{}", quiet.pattern);
}
}
}