bob_adapters/
policy_static.rs1use bob_core::{is_tool_allowed, merge_allowlists, normalize_tool_list, ports::ToolPolicyPort};
4
5#[derive(Debug, Clone, Default)]
7pub struct StaticToolPolicyPort {
8 runtime_deny_tools: Vec<String>,
9 runtime_allow_tools: Option<Vec<String>>,
10 default_deny: bool,
11}
12
13impl StaticToolPolicyPort {
14 #[must_use]
15 pub fn new(
16 runtime_deny_tools: Vec<String>,
17 runtime_allow_tools: Option<Vec<String>>,
18 default_deny: bool,
19 ) -> Self {
20 Self {
21 runtime_deny_tools: normalize_tool_list(runtime_deny_tools.iter().map(String::as_str)),
22 runtime_allow_tools: runtime_allow_tools
23 .map(|tools| normalize_tool_list(tools.iter().map(String::as_str))),
24 default_deny,
25 }
26 }
27}
28
29impl ToolPolicyPort for StaticToolPolicyPort {
30 fn is_tool_allowed(
31 &self,
32 tool: &str,
33 deny_tools: &[String],
34 allow_tools: Option<&[String]>,
35 ) -> bool {
36 let effective_deny = normalize_tool_list(
37 self.runtime_deny_tools
38 .iter()
39 .map(String::as_str)
40 .chain(deny_tools.iter().map(String::as_str)),
41 );
42 let effective_allow = merge_allowlists(self.runtime_allow_tools.as_deref(), allow_tools);
43 if self.default_deny && effective_allow.is_none() {
44 return false;
45 }
46 is_tool_allowed(tool, &effective_deny, effective_allow.as_deref())
47 }
48}
49
50#[cfg(test)]
51mod tests {
52 use super::*;
53
54 #[test]
55 fn runtime_deny_blocks_tool_even_when_request_allows() {
56 let policy = StaticToolPolicyPort::new(
57 vec!["local/shell_exec".to_string()],
58 Some(vec!["local/shell_exec".to_string(), "local/read_file".to_string()]),
59 false,
60 );
61 let request_allow = vec!["local/shell_exec".to_string()];
62 assert!(!policy.is_tool_allowed("local/shell_exec", &[], Some(request_allow.as_slice())));
63 }
64
65 #[test]
66 fn default_deny_requires_effective_allowlist() {
67 let policy = StaticToolPolicyPort::new(vec![], None, true);
68 assert!(!policy.is_tool_allowed("local/read_file", &[], None));
69
70 let request_allow = vec!["local/read_file".to_string()];
71 assert!(policy.is_tool_allowed("local/read_file", &[], Some(request_allow.as_slice())));
72 }
73
74 #[test]
75 fn runtime_and_request_allowlists_are_intersected() {
76 let policy = StaticToolPolicyPort::new(
77 vec![],
78 Some(vec!["local/read_file".to_string(), "local/write_file".to_string()]),
79 false,
80 );
81 let request_allow = vec!["local/read_file".to_string()];
82 assert!(policy.is_tool_allowed("local/read_file", &[], Some(request_allow.as_slice())));
83 assert!(!policy.is_tool_allowed("local/write_file", &[], Some(request_allow.as_slice())));
84 }
85}