use crate::agent::state::StopReason;
use std::collections::HashMap;
#[derive(Default)]
pub struct PolicyContext {
pub elapsed_ms: u64,
pub now_ms: u64,
pub requested_tool: Option<String>,
pub tokens_spent: u64,
pub tool_call_counts: HashMap<String, u32>,
pub tool_call_history: Vec<String>,
pub tool_labels: HashMap<String, Vec<String>>,
pub last_tool_args: HashMap<String, String>,
}
impl PolicyContext {
pub fn tool_has(&self, tool: &str, label: &str) -> bool {
self.tool_labels
.get(tool)
.is_some_and(|labels| labels.iter().any(|l| l == label))
}
pub fn tools_with(&self, label: &str) -> Vec<&str> {
let mut out: Vec<&str> = self
.tool_labels
.iter()
.filter(|(_, labels)| labels.iter().any(|l| l == label))
.map(|(name, _)| name.as_str())
.collect();
out.sort_unstable();
out
}
}
pub enum PolicyDecision {
Allow,
Deny { reason: StopReason },
}
pub trait Policy {
fn evaluate(&self, context: &PolicyContext) -> PolicyDecision;
}
#[cfg(test)]
mod tests {
use super::*;
fn ctx_with_labels() -> PolicyContext {
let mut tool_labels = HashMap::new();
tool_labels.insert(
"web_search".to_string(),
vec!["reads_untrusted".to_string()],
);
tool_labels.insert(
"send_outreach".to_string(),
vec!["external_effect".to_string(), "moves_money".to_string()],
);
tool_labels.insert("save_findings".to_string(), Vec::new());
PolicyContext {
tool_labels,
..Default::default()
}
}
#[test]
fn tool_has_finds_a_declared_label() {
let ctx = ctx_with_labels();
assert!(ctx.tool_has("web_search", "reads_untrusted"));
assert!(ctx.tool_has("send_outreach", "moves_money"));
}
#[test]
fn tool_has_is_false_for_a_label_the_tool_lacks() {
assert!(!ctx_with_labels().tool_has("web_search", "moves_money"));
}
#[test]
fn tool_has_is_false_for_an_unlabelled_tool() {
assert!(!ctx_with_labels().tool_has("save_findings", "external_effect"));
}
#[test]
fn tool_has_is_false_for_an_unknown_tool() {
assert!(!ctx_with_labels().tool_has("ghost", "reads_untrusted"));
}
#[test]
fn tool_has_is_false_for_an_unknown_label() {
assert!(!ctx_with_labels().tool_has("web_search", "reads_untrused"));
}
#[test]
fn tool_has_is_false_on_a_default_context() {
assert!(!PolicyContext::default().tool_has("anything", "destructive"));
}
#[test]
fn tools_with_collects_every_tool_carrying_a_label() {
let mut ctx = ctx_with_labels();
ctx.tool_labels
.insert("charge_card".to_string(), vec!["moves_money".to_string()]);
assert_eq!(
ctx.tools_with("moves_money"),
vec!["charge_card", "send_outreach"]
);
}
#[test]
fn tools_with_is_sorted() {
let mut tool_labels = HashMap::new();
for name in ["zeta", "alpha", "mid"] {
tool_labels.insert(name.to_string(), vec!["destructive".to_string()]);
}
let ctx = PolicyContext {
tool_labels,
..Default::default()
};
assert_eq!(ctx.tools_with("destructive"), vec!["alpha", "mid", "zeta"]);
}
#[test]
fn tools_with_is_empty_for_an_unknown_label() {
assert!(ctx_with_labels().tools_with("nonexistent").is_empty());
}
}