Skip to main content

bob_adapters/
policy_static.rs

1//! Static tool-policy adapter composed from runtime and request policies.
2
3use bob_core::{is_tool_allowed, merge_allowlists, normalize_tool_list, ports::ToolPolicyPort};
4
5/// Runtime-level static tool policy.
6#[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}