bob_adapters/
approval_static.rs1use bob_core::{
4 ToolError, normalize_tool_list,
5 ports::ApprovalPort,
6 tools_match,
7 types::{ApprovalContext, ApprovalDecision, ToolCall},
8};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum StaticApprovalMode {
13 AllowAll,
14 DenyAll,
15}
16
17#[derive(Debug, Clone)]
19pub struct StaticApprovalPort {
20 mode: StaticApprovalMode,
21 deny_tools: Vec<String>,
22}
23
24impl StaticApprovalPort {
25 #[must_use]
26 pub fn new(mode: StaticApprovalMode, deny_tools: Vec<String>) -> Self {
27 Self { mode, deny_tools: normalize_tool_list(deny_tools.iter().map(String::as_str)) }
28 }
29}
30
31#[async_trait::async_trait]
32impl ApprovalPort for StaticApprovalPort {
33 async fn approve_tool_call(
34 &self,
35 call: &ToolCall,
36 _context: &ApprovalContext,
37 ) -> Result<ApprovalDecision, ToolError> {
38 if self.mode == StaticApprovalMode::DenyAll {
39 return Ok(ApprovalDecision::Denied {
40 reason: "tool calls are disabled by approval policy".to_string(),
41 });
42 }
43 if self.deny_tools.iter().any(|tool| tools_match(tool, &call.name)) {
44 return Ok(ApprovalDecision::Denied {
45 reason: format!("tool '{}' denied by approval policy", call.name),
46 });
47 }
48 Ok(ApprovalDecision::Approved)
49 }
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55
56 fn make_tool_call(name: &str) -> ToolCall {
57 ToolCall { name: name.to_string(), arguments: serde_json::json!({}) }
58 }
59
60 fn test_context() -> ApprovalContext {
61 ApprovalContext { session_id: "s1".to_string(), turn_step: 1, selected_skills: Vec::new() }
62 }
63
64 #[tokio::test]
65 async fn deny_all_rejects_everything() {
66 let approval = StaticApprovalPort::new(StaticApprovalMode::DenyAll, vec![]);
67 let decision =
68 approval.approve_tool_call(&make_tool_call("local/read_file"), &test_context()).await;
69 assert!(decision.is_ok());
70 assert!(matches!(decision.ok(), Some(ApprovalDecision::Denied { .. })));
71 }
72
73 #[tokio::test]
74 async fn deny_tools_rejects_listed_tools() {
75 let approval =
76 StaticApprovalPort::new(StaticApprovalMode::AllowAll, vec!["local/shell_exec".into()]);
77 let decision =
78 approval.approve_tool_call(&make_tool_call("local/shell_exec"), &test_context()).await;
79 assert!(decision.is_ok());
80 assert!(matches!(decision.ok(), Some(ApprovalDecision::Denied { .. })));
81 }
82
83 #[tokio::test]
84 async fn allow_mode_allows_unlisted_tools() {
85 let approval =
86 StaticApprovalPort::new(StaticApprovalMode::AllowAll, vec!["local/shell_exec".into()]);
87 let decision =
88 approval.approve_tool_call(&make_tool_call("local/read_file"), &test_context()).await;
89 assert!(decision.is_ok());
90 assert!(matches!(decision.ok(), Some(ApprovalDecision::Approved)));
91 }
92}