Skip to main content

aivcs_core/sandbox/
engine.rs

1//! Policy evaluation engine — first-match-wins, default-deny.
2
3use super::policy::ToolPolicySet;
4use super::request::{PolicyVerdict, ToolRequest};
5
6/// Evaluate a [`ToolRequest`] against a [`ToolPolicySet`].
7///
8/// Rules are checked in order. The first rule whose (role, capability) pair
9/// matches determines the verdict. If no rule matches, the request is **denied**
10/// (default-deny posture).
11pub fn evaluate_tool_request(policy: &ToolPolicySet, request: &ToolRequest) -> PolicyVerdict {
12    for rule in &policy.rules {
13        if rule.matches(&request.requesting_role, &request.capability) {
14            return rule.verdict();
15        }
16    }
17
18    // Default-deny
19    PolicyVerdict::Denied {
20        reason: format!(
21            "no policy rule matched role={} capability={:?}",
22            request.requesting_role, request.capability,
23        ),
24    }
25}
26
27#[cfg(test)]
28mod tests {
29    use super::*;
30    use crate::role_orchestration::roles::AgentRole;
31    use crate::sandbox::capability::ToolCapability;
32    use crate::sandbox::policy::{ToolPolicyRule, ToolPolicySet};
33
34    fn make_request(role: AgentRole, capability: ToolCapability) -> ToolRequest {
35        ToolRequest {
36            tool_name: "test_tool".into(),
37            capability,
38            params: serde_json::Value::Null,
39            requesting_role: role,
40        }
41    }
42
43    #[test]
44    fn test_default_deny_when_no_rules() {
45        let policy = ToolPolicySet::empty();
46        let req = make_request(AgentRole::Coder, ToolCapability::ShellExec);
47        let v = evaluate_tool_request(&policy, &req);
48        assert!(!v.is_allowed());
49    }
50
51    #[test]
52    fn test_first_match_wins() {
53        // Deny first, then allow — deny wins.
54        let policy = ToolPolicySet::empty()
55            .with_rule(ToolPolicyRule::Deny {
56                role: AgentRole::Coder,
57                capability: ToolCapability::ShellExec,
58                reason: "denied first".into(),
59            })
60            .with_rule(ToolPolicyRule::Allow {
61                role: AgentRole::Coder,
62                capability: ToolCapability::ShellExec,
63            });
64
65        let req = make_request(AgentRole::Coder, ToolCapability::ShellExec);
66        match evaluate_tool_request(&policy, &req) {
67            PolicyVerdict::Denied { reason } => assert!(reason.contains("denied first")),
68            other => panic!("expected Denied, got {:?}", other),
69        }
70    }
71
72    #[test]
73    fn test_standard_dev_coder_allowed_shell() {
74        let policy = ToolPolicySet::standard_dev();
75        let req = make_request(AgentRole::Coder, ToolCapability::ShellExec);
76        assert!(evaluate_tool_request(&policy, &req).is_allowed());
77    }
78
79    #[test]
80    fn test_standard_dev_reviewer_denied_shell() {
81        let policy = ToolPolicySet::standard_dev();
82        let req = make_request(AgentRole::Reviewer, ToolCapability::ShellExec);
83        assert!(!evaluate_tool_request(&policy, &req).is_allowed());
84    }
85
86    #[test]
87    fn test_standard_dev_all_roles_denied_http_fetch() {
88        let policy = ToolPolicySet::standard_dev();
89        for role in &[
90            AgentRole::Planner,
91            AgentRole::Coder,
92            AgentRole::Reviewer,
93            AgentRole::Tester,
94            AgentRole::Fixer,
95        ] {
96            let req = make_request(role.clone(), ToolCapability::NetworkFetch);
97            assert!(
98                !evaluate_tool_request(&policy, &req).is_allowed(),
99                "expected HttpFetch denied for {role}"
100            );
101        }
102    }
103
104    #[test]
105    fn test_require_approval_verdict() {
106        let policy = ToolPolicySet::empty().with_rule(ToolPolicyRule::RequireApproval {
107            role: AgentRole::Coder,
108            capability: ToolCapability::NetworkFetch,
109            reason: "network access needs approval".into(),
110        });
111        let req = make_request(AgentRole::Coder, ToolCapability::NetworkFetch);
112        match evaluate_tool_request(&policy, &req) {
113            PolicyVerdict::RequiresApproval { reason } => {
114                assert!(reason.contains("network access"));
115            }
116            other => panic!("expected RequiresApproval, got {:?}", other),
117        }
118    }
119}