use serde::{Deserialize, Serialize};
use crate::role_orchestration::roles::AgentRole;
use super::capability::ToolCapability;
use super::request::PolicyVerdict;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ToolPolicyRule {
Allow {
role: AgentRole,
capability: ToolCapability,
},
Deny {
role: AgentRole,
capability: ToolCapability,
reason: String,
},
RequireApproval {
role: AgentRole,
capability: ToolCapability,
reason: String,
},
}
impl ToolPolicyRule {
pub fn matches(&self, role: &AgentRole, capability: &ToolCapability) -> bool {
match self {
ToolPolicyRule::Allow {
role: r,
capability: c,
}
| ToolPolicyRule::Deny {
role: r,
capability: c,
..
}
| ToolPolicyRule::RequireApproval {
role: r,
capability: c,
..
} => r == role && c == capability,
}
}
pub fn verdict(&self) -> PolicyVerdict {
match self {
ToolPolicyRule::Allow { .. } => PolicyVerdict::Allowed,
ToolPolicyRule::Deny { reason, .. } => PolicyVerdict::Denied {
reason: reason.clone(),
},
ToolPolicyRule::RequireApproval { reason, .. } => PolicyVerdict::RequiresApproval {
reason: reason.clone(),
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolPolicySet {
pub rules: Vec<ToolPolicyRule>,
}
impl ToolPolicySet {
pub fn empty() -> Self {
Self { rules: Vec::new() }
}
pub fn with_rule(mut self, rule: ToolPolicyRule) -> Self {
self.rules.push(rule);
self
}
pub fn standard_dev() -> Self {
let mut rules = Vec::new();
let allow = |rules: &mut Vec<ToolPolicyRule>, role: AgentRole, caps: &[ToolCapability]| {
for cap in caps {
rules.push(ToolPolicyRule::Allow {
role: role.clone(),
capability: cap.clone(),
});
}
};
let read_only = &[ToolCapability::FileRead, ToolCapability::GitRead];
allow(&mut rules, AgentRole::Planner, read_only);
allow(
&mut rules,
AgentRole::Coder,
&[
ToolCapability::FileRead,
ToolCapability::FileWrite,
ToolCapability::GitRead,
ToolCapability::GitWrite,
ToolCapability::ShellExec,
],
);
allow(&mut rules, AgentRole::Reviewer, read_only);
allow(
&mut rules,
AgentRole::Tester,
&[
ToolCapability::FileRead,
ToolCapability::GitRead,
ToolCapability::ShellExec,
],
);
allow(
&mut rules,
AgentRole::Fixer,
&[
ToolCapability::FileRead,
ToolCapability::FileWrite,
ToolCapability::GitRead,
ToolCapability::GitWrite,
ToolCapability::ShellExec,
],
);
Self { rules }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rule_matches_correct_pair() {
let rule = ToolPolicyRule::Allow {
role: AgentRole::Coder,
capability: ToolCapability::ShellExec,
};
assert!(rule.matches(&AgentRole::Coder, &ToolCapability::ShellExec));
assert!(!rule.matches(&AgentRole::Reviewer, &ToolCapability::ShellExec));
assert!(!rule.matches(&AgentRole::Coder, &ToolCapability::NetworkFetch));
}
#[test]
fn test_deny_rule_verdict() {
let rule = ToolPolicyRule::Deny {
role: AgentRole::Planner,
capability: ToolCapability::ShellExec,
reason: "planners cannot shell".into(),
};
assert!(rule.matches(&AgentRole::Planner, &ToolCapability::ShellExec));
match rule.verdict() {
PolicyVerdict::Denied { reason } => {
assert!(reason.contains("planners cannot shell"));
}
other => panic!("expected Denied, got {:?}", other),
}
}
#[test]
fn test_standard_dev_has_rules() {
let policy = ToolPolicySet::standard_dev();
assert_eq!(policy.rules.len(), 17);
}
#[test]
fn test_with_rule_appends() {
let policy = ToolPolicySet::empty().with_rule(ToolPolicyRule::Allow {
role: AgentRole::Coder,
capability: ToolCapability::NetworkFetch,
});
assert_eq!(policy.rules.len(), 1);
}
#[test]
fn test_policy_serde_roundtrip() {
let policy = ToolPolicySet::standard_dev();
let json = serde_json::to_string(&policy).unwrap();
let back: ToolPolicySet = serde_json::from_str(&json).unwrap();
assert_eq!(policy, back);
}
}