#![allow(dead_code)]
use aa_core::{GovernanceAction, GovernanceLevel};
use crate::policy::context::PolicyContext;
use strsim;
pub(crate) const KNOWN_VARIABLES: &[&str] = &[
"tool",
"path",
"url",
"method",
"command",
"governance_level",
"agent.depth",
"agent.risk_tier",
"team.active_agents",
"team.budget_remaining",
"child.tool",
"child.risk_tier",
"parent.risk_tier",
"source.team_id",
"target.team_id",
"target.channel_id",
"agent.age",
"team.parallel_agents",
"agent.parent_agent_id",
"agent.team_id",
"agent.children_count",
"agent.is_root",
"agent.is_leaf",
];
#[derive(Debug, PartialEq)]
enum FieldRef {
Tool,
Path,
Url,
Method,
Command,
GovernanceLevel,
AgentDepth,
TeamActiveAgents,
TeamBudgetRemaining,
ChildTool,
ChildRiskTier,
AgentRiskTier,
ParentRiskTier,
SourceTeamId,
TargetTeamId,
TargetChannelId,
AgentAge,
TeamParallelAgents,
AgentParentId,
AgentTeamId,
AgentChildrenCount,
AgentIsRoot,
AgentIsLeaf,
ToolArg(String),
ToolResult(String),
ToolResultWhole,
}
#[derive(Debug, PartialEq)]
enum OpKind {
Eq,
Ne,
Gt,
Gte,
Lt,
Lte,
Contains,
StartsWith,
In,
NotIn,
}
#[derive(Debug, PartialEq)]
enum LiteralVal {
Str(String),
Num(f64),
Level(GovernanceLevel),
Tier(aa_core::RiskTier),
StrList(Vec<String>),
Duration(u64),
}
#[derive(Debug, PartialEq)]
enum Token {
Field(FieldRef),
Op(OpKind),
Literal(LiteralVal),
And,
Or,
}
fn tokenize(expr: &str) -> Option<Vec<Token>> {
let mut tokens = Vec::new();
let mut chars = expr.chars().peekable();
while let Some(&ch) = chars.peek() {
if ch.is_whitespace() {
chars.next();
continue;
}
if ch == '[' {
chars.next(); let mut items: Vec<String> = Vec::new();
loop {
while let Some(&c) = chars.peek() {
if c.is_whitespace() {
chars.next();
} else {
break;
}
}
match chars.peek() {
Some(&']') => {
chars.next();
break;
}
Some(&'"') => {
chars.next(); let mut s = String::new();
loop {
match chars.next() {
Some('"') => break,
Some('\\') => match chars.next() {
Some('"') => s.push('"'),
Some('\\') => s.push('\\'),
Some(c) => {
s.push('\\');
s.push(c);
}
None => return None,
},
Some(c) => s.push(c),
None => return None,
}
}
items.push(s);
while let Some(&c) = chars.peek() {
if c.is_whitespace() || c == ',' {
chars.next();
} else {
break;
}
}
}
_ => return None, }
}
tokens.push(Token::Literal(LiteralVal::StrList(items)));
continue;
}
if ch == '"' {
chars.next(); let mut s = String::new();
loop {
match chars.next() {
Some('"') => break,
Some('\\') => {
match chars.next() {
Some('"') => s.push('"'),
Some('\\') => s.push('\\'),
Some(c) => {
s.push('\\');
s.push(c);
}
None => return None, }
}
Some(c) => s.push(c),
None => return None, }
}
tokens.push(Token::Literal(LiteralVal::Str(s)));
continue;
}
if ch == '<' || ch == '>' || ch == '=' || ch == '!' {
chars.next();
let op = if chars.peek() == Some(&'=') {
chars.next();
match ch {
'<' => OpKind::Lte,
'>' => OpKind::Gte,
'=' => OpKind::Eq,
'!' => OpKind::Ne,
_ => return None,
}
} else {
match ch {
'<' => OpKind::Lt,
'>' => OpKind::Gt,
_ => return None, }
};
tokens.push(Token::Op(op));
continue;
}
if ch.is_alphanumeric() || ch == '_' || ch == '-' || ch == '.' {
let mut word = String::new();
while let Some(&c) = chars.peek() {
if c.is_alphanumeric() || c == '_' || c == '-' || c == '.' {
word.push(c);
chars.next();
} else {
break;
}
}
if let Some(rest) = word.strip_prefix("args.") {
if rest.is_empty() {
return None;
}
let pointer = format!("/{}", rest.replace('.', "/"));
tokens.push(Token::Field(FieldRef::ToolArg(pointer)));
continue;
}
if let Some(rest) = word.strip_prefix("tool_result.") {
if rest.is_empty() {
return None;
}
let pointer = format!("/{}", rest.replace('.', "/"));
tokens.push(Token::Field(FieldRef::ToolResult(pointer)));
continue;
}
if word == "tool_result" {
tokens.push(Token::Field(FieldRef::ToolResultWhole));
continue;
}
let token = match word.as_str() {
"AND" => Token::And,
"OR" => Token::Or,
"tool" => Token::Field(FieldRef::Tool),
"path" => Token::Field(FieldRef::Path),
"url" => Token::Field(FieldRef::Url),
"method" => Token::Field(FieldRef::Method),
"command" => Token::Field(FieldRef::Command),
"governance_level" => Token::Field(FieldRef::GovernanceLevel),
"agent.depth" => Token::Field(FieldRef::AgentDepth),
"team.active_agents" => Token::Field(FieldRef::TeamActiveAgents),
"team.budget_remaining" => Token::Field(FieldRef::TeamBudgetRemaining),
"child.tool" => Token::Field(FieldRef::ChildTool),
"child.risk_tier" => Token::Field(FieldRef::ChildRiskTier),
"agent.risk_tier" => Token::Field(FieldRef::AgentRiskTier),
"parent.risk_tier" => Token::Field(FieldRef::ParentRiskTier),
"source.team_id" => Token::Field(FieldRef::SourceTeamId),
"target.team_id" => Token::Field(FieldRef::TargetTeamId),
"target.channel_id" => Token::Field(FieldRef::TargetChannelId),
"agent.age" => Token::Field(FieldRef::AgentAge),
"team.parallel_agents" => Token::Field(FieldRef::TeamParallelAgents),
"agent.parent_agent_id" => Token::Field(FieldRef::AgentParentId),
"agent.team_id" => Token::Field(FieldRef::AgentTeamId),
"agent.children_count" => Token::Field(FieldRef::AgentChildrenCount),
"agent.is_root" => Token::Field(FieldRef::AgentIsRoot),
"agent.is_leaf" => Token::Field(FieldRef::AgentIsLeaf),
"L0" => Token::Literal(LiteralVal::Level(GovernanceLevel::L0Discover)),
"L1" => Token::Literal(LiteralVal::Level(GovernanceLevel::L1Observe)),
"L2" => Token::Literal(LiteralVal::Level(GovernanceLevel::L2Enforce)),
"L3" => Token::Literal(LiteralVal::Level(GovernanceLevel::L3Native)),
"Low" => Token::Literal(LiteralVal::Tier(aa_core::RiskTier::Low)),
"Medium" => Token::Literal(LiteralVal::Tier(aa_core::RiskTier::Medium)),
"High" => Token::Literal(LiteralVal::Tier(aa_core::RiskTier::High)),
"Critical" => Token::Literal(LiteralVal::Tier(aa_core::RiskTier::Critical)),
"contains" => Token::Op(OpKind::Contains),
"starts_with" => Token::Op(OpKind::StartsWith),
"in" => Token::Op(OpKind::In),
"not_in" => Token::Op(OpKind::NotIn),
other => {
if let Ok(n) = other.parse::<f64>() {
Token::Literal(LiteralVal::Num(n))
} else if other.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) {
if let Ok(d) = humantime::parse_duration(other) {
Token::Literal(LiteralVal::Duration(d.as_secs()))
} else {
return None;
}
} else {
return None; }
}
};
tokens.push(token);
continue;
}
return None;
}
Some(tokens)
}
fn field_value<'a>(field: &FieldRef, action: &'a GovernanceAction) -> &'a str {
match (field, action) {
(FieldRef::Tool, GovernanceAction::ToolCall { name, .. }) => name.as_str(),
(FieldRef::Path, GovernanceAction::FileAccess { path, .. }) => path.as_str(),
(FieldRef::Url, GovernanceAction::NetworkRequest { url, .. }) => url.as_str(),
(FieldRef::Method, GovernanceAction::NetworkRequest { method, .. }) => method.as_str(),
(FieldRef::Command, GovernanceAction::ProcessExec { command }) => command.as_str(),
_ => "",
}
}
fn eval_clause_safe(
field: &FieldRef,
op: &OpKind,
literal: &LiteralVal,
action: &GovernanceAction,
agent_level: Option<GovernanceLevel>,
policy_ctx: Option<&dyn PolicyContext>,
) -> bool {
if let FieldRef::AgentDepth = field {
let lhs = match policy_ctx.and_then(|c| c.agent_depth()) {
Some(d) => d as f64,
None => return false,
};
let rhs = match numeric_literal(literal) {
Some(r) => r,
None => return false,
};
return match op {
OpKind::Eq => lhs == rhs,
OpKind::Ne => lhs != rhs,
OpKind::Gt => lhs > rhs,
OpKind::Gte => lhs >= rhs,
OpKind::Lt => lhs < rhs,
OpKind::Lte => lhs <= rhs,
OpKind::Contains | OpKind::StartsWith | OpKind::In | OpKind::NotIn => false,
};
}
if let FieldRef::TeamActiveAgents = field {
let lhs = match policy_ctx.and_then(|c| c.team_active_agents()) {
Some(n) => n as f64,
None => return false,
};
let rhs = match numeric_literal(literal) {
Some(r) => r,
None => return false,
};
return match op {
OpKind::Eq => lhs == rhs,
OpKind::Ne => lhs != rhs,
OpKind::Gt => lhs > rhs,
OpKind::Gte => lhs >= rhs,
OpKind::Lt => lhs < rhs,
OpKind::Lte => lhs <= rhs,
OpKind::Contains | OpKind::StartsWith | OpKind::In | OpKind::NotIn => false,
};
}
if let FieldRef::TeamBudgetRemaining = field {
let lhs = match policy_ctx.and_then(|c| c.team_budget_remaining()) {
Some(r) => r,
None => return false,
};
let rhs = match numeric_literal(literal) {
Some(r) => r,
None => return false,
};
return match op {
OpKind::Eq => lhs == rhs,
OpKind::Ne => lhs != rhs,
OpKind::Gt => lhs > rhs,
OpKind::Gte => lhs >= rhs,
OpKind::Lt => lhs < rhs,
OpKind::Lte => lhs <= rhs,
OpKind::Contains | OpKind::StartsWith | OpKind::In | OpKind::NotIn => false,
};
}
if let FieldRef::ChildTool = field {
let tools = match policy_ctx {
Some(c) => c.child_tools(),
None => return false,
};
let rhs = match literal {
LiteralVal::Str(s) => s.as_str(),
_ => return false,
};
return match op {
OpKind::Eq => tools.iter().any(|t| t == rhs),
OpKind::Ne => tools.iter().all(|t| t != rhs),
OpKind::Contains => tools.iter().any(|t| t.contains(rhs)),
OpKind::StartsWith => tools.iter().any(|t| t.starts_with(rhs)),
_ => false,
};
}
if matches!(field, FieldRef::ToolResult(_) | FieldRef::ToolResultWhole) {
let result_str = match action {
GovernanceAction::ToolResult { result, .. } => result.as_str(),
_ => return false,
};
if let FieldRef::ToolResultWhole = field {
let lit = match literal {
LiteralVal::Str(s) => s.as_str(),
_ => return false,
};
return match op {
OpKind::Contains => result_str.contains(lit),
OpKind::StartsWith => result_str.starts_with(lit),
_ => false,
};
}
let pointer = match field {
FieldRef::ToolResult(p) => p,
_ => unreachable!(),
};
let result_value: serde_json::Value = match serde_json::from_str(result_str) {
Ok(v) => v,
Err(_) => return false,
};
let resolved = match result_value.pointer(pointer) {
Some(v) => v,
None => return false,
};
return match op {
OpKind::Eq | OpKind::Ne => match (resolved, literal) {
(serde_json::Value::String(s), LiteralVal::Str(lit)) => {
if matches!(op, OpKind::Eq) {
s == lit
} else {
s != lit
}
}
(serde_json::Value::Number(n), LiteralVal::Num(lit)) => match n.as_f64() {
Some(v) => {
if matches!(op, OpKind::Eq) {
(v - *lit).abs() < f64::EPSILON
} else {
(v - *lit).abs() >= f64::EPSILON
}
}
None => false,
},
_ => false,
},
OpKind::Contains | OpKind::StartsWith => {
let value = match resolved.as_str() {
Some(s) => s,
None => return false,
};
let lit = match literal {
LiteralVal::Str(s) => s.as_str(),
_ => return false,
};
if matches!(op, OpKind::Contains) {
value.contains(lit)
} else {
value.starts_with(lit)
}
}
OpKind::In | OpKind::NotIn => {
let value = match resolved.as_str() {
Some(s) => s,
None => return false,
};
let list = match literal {
LiteralVal::StrList(items) => items,
_ => return false,
};
if matches!(op, OpKind::In) {
list.iter().any(|item| item == value)
} else {
list.iter().all(|item| item != value)
}
}
OpKind::Gt | OpKind::Gte | OpKind::Lt | OpKind::Lte => {
let lhs = match resolved.as_f64() {
Some(n) => n,
None => return false,
};
let rhs = match literal {
LiteralVal::Num(n) => *n,
_ => return false,
};
match op {
OpKind::Gt => lhs > rhs,
OpKind::Gte => lhs >= rhs,
OpKind::Lt => lhs < rhs,
OpKind::Lte => lhs <= rhs,
_ => unreachable!(),
}
}
};
}
if let FieldRef::ToolArg(pointer) = field {
let args_str = match action {
GovernanceAction::ToolCall { args, .. } => args.as_str(),
_ => return false,
};
let args_value: serde_json::Value = match serde_json::from_str(args_str) {
Ok(v) => v,
Err(_) => return false,
};
let resolved = match args_value.pointer(pointer) {
Some(v) => v,
None => return false,
};
return match op {
OpKind::Eq | OpKind::Ne => match (resolved, literal) {
(serde_json::Value::String(s), LiteralVal::Str(lit)) => {
if matches!(op, OpKind::Eq) {
s == lit
} else {
s != lit
}
}
(serde_json::Value::Number(n), LiteralVal::Num(lit)) => match n.as_f64() {
Some(v) => {
if matches!(op, OpKind::Eq) {
(v - *lit).abs() < f64::EPSILON
} else {
(v - *lit).abs() >= f64::EPSILON
}
}
None => false,
},
_ => false,
},
OpKind::Contains | OpKind::StartsWith => {
let value = match resolved.as_str() {
Some(s) => s,
None => return false,
};
let lit = match literal {
LiteralVal::Str(s) => s.as_str(),
_ => return false,
};
if matches!(op, OpKind::Contains) {
value.contains(lit)
} else {
value.starts_with(lit)
}
}
OpKind::In | OpKind::NotIn => {
let value = match resolved.as_str() {
Some(s) => s,
None => return false,
};
let list = match literal {
LiteralVal::StrList(items) => items,
_ => return false,
};
if matches!(op, OpKind::In) {
list.iter().any(|item| item == value)
} else {
list.iter().all(|item| item != value)
}
}
OpKind::Gt | OpKind::Gte | OpKind::Lt | OpKind::Lte => {
let lhs = match resolved.as_f64() {
Some(n) => n,
None => return false,
};
let rhs = match literal {
LiteralVal::Num(n) => *n,
_ => return false,
};
match op {
OpKind::Gt => lhs > rhs,
OpKind::Gte => lhs >= rhs,
OpKind::Lt => lhs < rhs,
OpKind::Lte => lhs <= rhs,
_ => unreachable!(),
}
}
};
}
if let FieldRef::AgentRiskTier = field {
let lhs = match policy_ctx.and_then(|c| c.agent_risk_tier()) {
Some(t) => t,
None => return false,
};
let rhs = match literal {
LiteralVal::Tier(t) => *t,
_ => return false,
};
return match op {
OpKind::Eq => lhs == rhs,
OpKind::Ne => lhs != rhs,
OpKind::Gt => lhs > rhs,
OpKind::Gte => lhs >= rhs,
OpKind::Lt => lhs < rhs,
OpKind::Lte => lhs <= rhs,
OpKind::Contains | OpKind::StartsWith | OpKind::In | OpKind::NotIn => false,
};
}
if let FieldRef::ParentRiskTier = field {
let lhs = match policy_ctx.and_then(|c| c.parent_risk_tier()) {
Some(t) => t,
None => return false,
};
let rhs = match literal {
LiteralVal::Tier(t) => *t,
_ => return false,
};
return match op {
OpKind::Eq => lhs == rhs,
OpKind::Ne => lhs != rhs,
OpKind::Gt => lhs > rhs,
OpKind::Gte => lhs >= rhs,
OpKind::Lt => lhs < rhs,
OpKind::Lte => lhs <= rhs,
OpKind::Contains | OpKind::StartsWith | OpKind::In | OpKind::NotIn => false,
};
}
if let FieldRef::ChildRiskTier = field {
let lhs = match policy_ctx.and_then(|c| c.child_risk_tier()) {
Some(t) => t,
None => return false,
};
let rhs = match literal {
LiteralVal::Tier(t) => *t,
_ => return false,
};
return match op {
OpKind::Eq => lhs == rhs,
OpKind::Ne => lhs != rhs,
OpKind::Gt => lhs > rhs,
OpKind::Gte => lhs >= rhs,
OpKind::Lt => lhs < rhs,
OpKind::Lte => lhs <= rhs,
OpKind::Contains | OpKind::StartsWith | OpKind::In | OpKind::NotIn => false,
};
}
if let FieldRef::TeamParallelAgents = field {
let lhs = match policy_ctx.and_then(|c| c.team_active_agents()) {
Some(n) => n as f64,
None => return false,
};
let rhs = match numeric_literal(literal) {
Some(r) => r,
None => return false,
};
return match op {
OpKind::Eq => lhs == rhs,
OpKind::Ne => lhs != rhs,
OpKind::Gt => lhs > rhs,
OpKind::Gte => lhs >= rhs,
OpKind::Lt => lhs < rhs,
OpKind::Lte => lhs <= rhs,
OpKind::Contains | OpKind::StartsWith | OpKind::In | OpKind::NotIn => false,
};
}
if let FieldRef::AgentAge = field {
let lhs = match policy_ctx.and_then(|c| c.agent_age_secs()) {
Some(age) => age as f64,
None => return false,
};
let rhs = match numeric_literal(literal) {
Some(r) => r,
None => return false,
};
return match op {
OpKind::Eq => lhs == rhs,
OpKind::Ne => lhs != rhs,
OpKind::Gt => lhs > rhs,
OpKind::Gte => lhs >= rhs,
OpKind::Lt => lhs < rhs,
OpKind::Lte => lhs <= rhs,
OpKind::Contains | OpKind::StartsWith | OpKind::In | OpKind::NotIn => false,
};
}
if matches!(field, FieldRef::AgentParentId | FieldRef::AgentTeamId) {
let val = match field {
FieldRef::AgentParentId => policy_ctx.and_then(|c| c.agent_parent_id()),
FieldRef::AgentTeamId => policy_ctx.and_then(|c| c.agent_team_id()),
_ => unreachable!(),
};
let id = match val {
Some(v) => v,
None => return false,
};
let rhs = match literal {
LiteralVal::Str(s) => s.as_str(),
_ => return false,
};
return match op {
OpKind::Eq => id == rhs,
OpKind::Ne => id != rhs,
OpKind::Contains => id.contains(rhs),
OpKind::StartsWith => id.starts_with(rhs),
_ => false,
};
}
if let FieldRef::AgentChildrenCount = field {
let lhs = match policy_ctx.and_then(|c| c.agent_children_count()) {
Some(n) => n as f64,
None => return false,
};
let rhs = match numeric_literal(literal) {
Some(r) => r,
None => return false,
};
return match op {
OpKind::Eq => lhs == rhs,
OpKind::Ne => lhs != rhs,
OpKind::Gt => lhs > rhs,
OpKind::Gte => lhs >= rhs,
OpKind::Lt => lhs < rhs,
OpKind::Lte => lhs <= rhs,
OpKind::Contains | OpKind::StartsWith | OpKind::In | OpKind::NotIn => false,
};
}
if matches!(field, FieldRef::AgentIsRoot | FieldRef::AgentIsLeaf) {
let flag: Option<bool> = match field {
FieldRef::AgentIsRoot => policy_ctx.and_then(|c| c.agent_depth()).map(|d| d == 0),
FieldRef::AgentIsLeaf => policy_ctx.and_then(|c| c.agent_children_count()).map(|n| n == 0),
_ => unreachable!(),
};
let lhs = match flag {
Some(v) => {
if v {
1.0_f64
} else {
0.0_f64
}
}
None => return false,
};
let rhs = match numeric_literal(literal) {
Some(r) => r,
None => return false,
};
return match op {
OpKind::Eq => lhs == rhs,
OpKind::Ne => lhs != rhs,
_ => false,
};
}
if let FieldRef::SourceTeamId = field {
let team_id = match action {
GovernanceAction::SendMessage { source_team_id, .. } => match source_team_id.as_deref() {
Some(id) => id.to_owned(),
None => return false,
},
_ => return false,
};
if *op == OpKind::In || *op == OpKind::NotIn {
if let LiteralVal::StrList(list) = literal {
return if *op == OpKind::In {
list.iter().any(|s| s == &team_id)
} else {
!list.iter().any(|s| s == &team_id)
};
}
return false;
}
let rhs = match literal {
LiteralVal::Str(s) => s.as_str(),
_ => return false,
};
return match op {
OpKind::Eq => team_id == rhs,
OpKind::Ne => team_id != rhs,
OpKind::Contains => team_id.contains(rhs),
OpKind::StartsWith => team_id.starts_with(rhs),
_ => false,
};
}
if let FieldRef::TargetTeamId = field {
let team_id = match action {
GovernanceAction::SendMessage { target_team_id, .. } => match target_team_id.as_deref() {
Some(id) => id.to_owned(),
None => return false,
},
_ => return false,
};
if *op == OpKind::In || *op == OpKind::NotIn {
if let LiteralVal::StrList(list) = literal {
return if *op == OpKind::In {
list.iter().any(|s| s == &team_id)
} else {
!list.iter().any(|s| s == &team_id)
};
}
return false;
}
let rhs = match literal {
LiteralVal::Str(s) => s.as_str(),
_ => return false,
};
return match op {
OpKind::Eq => team_id == rhs,
OpKind::Ne => team_id != rhs,
OpKind::Contains => team_id.contains(rhs),
OpKind::StartsWith => team_id.starts_with(rhs),
_ => false,
};
}
if let FieldRef::TargetChannelId = field {
let channel_id = match action {
GovernanceAction::SendMessage { channel_id, .. } => match channel_id.as_deref() {
Some(id) => id.to_owned(),
None => return false,
},
_ => return false,
};
if *op == OpKind::In || *op == OpKind::NotIn {
if let LiteralVal::StrList(list) = literal {
return if *op == OpKind::In {
list.iter().any(|s| s == &channel_id)
} else {
!list.iter().any(|s| s == &channel_id)
};
}
return false;
}
let rhs = match literal {
LiteralVal::Str(s) => s.as_str(),
_ => return false,
};
return match op {
OpKind::Eq => channel_id == rhs,
OpKind::Ne => channel_id != rhs,
OpKind::Contains => channel_id.contains(rhs),
OpKind::StartsWith => channel_id.starts_with(rhs),
_ => false,
};
}
if let FieldRef::GovernanceLevel = field {
let rhs = match literal {
LiteralVal::Level(l) => *l,
_ => return false,
};
let lhs = match agent_level {
Some(l) => l,
None => return false,
};
return match op {
OpKind::Eq => lhs == rhs,
OpKind::Ne => lhs != rhs,
OpKind::Gt => lhs > rhs,
OpKind::Gte => lhs >= rhs,
OpKind::Lt => lhs < rhs,
OpKind::Lte => lhs <= rhs,
OpKind::Contains | OpKind::StartsWith | OpKind::In | OpKind::NotIn => false,
};
}
let lhs = field_value(field, action);
match op {
OpKind::Contains => {
if let LiteralVal::Str(rhs) = literal {
lhs.contains(rhs.as_str())
} else {
false
}
}
OpKind::StartsWith => {
if let LiteralVal::Str(rhs) = literal {
lhs.starts_with(rhs.as_str())
} else {
false
}
}
OpKind::In => {
if let LiteralVal::StrList(list) = literal {
list.iter().any(|s| s.as_str() == lhs)
} else {
false
}
}
OpKind::NotIn => {
if let LiteralVal::StrList(list) = literal {
!list.iter().any(|s| s.as_str() == lhs)
} else {
false
}
}
OpKind::Eq => match literal {
LiteralVal::Num(rhs) => {
if let Ok(lhs_num) = lhs.parse::<f64>() {
lhs_num == *rhs
} else {
false
}
}
LiteralVal::Str(rhs) => lhs == rhs.as_str(),
LiteralVal::Level(_) | LiteralVal::Tier(_) | LiteralVal::StrList(_) | LiteralVal::Duration(_) => false,
},
OpKind::Ne => match literal {
LiteralVal::Num(rhs) => {
if let Ok(lhs_num) = lhs.parse::<f64>() {
lhs_num != *rhs
} else {
true }
}
LiteralVal::Str(rhs) => lhs != rhs.as_str(),
LiteralVal::Level(_) | LiteralVal::Tier(_) | LiteralVal::StrList(_) | LiteralVal::Duration(_) => true,
},
OpKind::Gt => {
let rhs = numeric_literal(literal);
let lhs_n = lhs.parse::<f64>().ok();
match (lhs_n, rhs) {
(Some(l), Some(r)) => l > r,
_ => false,
}
}
OpKind::Gte => {
let rhs = numeric_literal(literal);
let lhs_n = lhs.parse::<f64>().ok();
match (lhs_n, rhs) {
(Some(l), Some(r)) => l >= r,
_ => false,
}
}
OpKind::Lt => {
let rhs = numeric_literal(literal);
let lhs_n = lhs.parse::<f64>().ok();
match (lhs_n, rhs) {
(Some(l), Some(r)) => l < r,
_ => false,
}
}
OpKind::Lte => {
let rhs = numeric_literal(literal);
let lhs_n = lhs.parse::<f64>().ok();
match (lhs_n, rhs) {
(Some(l), Some(r)) => l <= r,
_ => false,
}
}
}
}
fn numeric_literal(lit: &LiteralVal) -> Option<f64> {
match lit {
LiteralVal::Num(n) => Some(*n),
LiteralVal::Str(s) => s.parse::<f64>().ok(),
LiteralVal::Duration(secs) => Some(*secs as f64),
LiteralVal::Level(_) | LiteralVal::Tier(_) | LiteralVal::StrList(_) => None,
}
}
struct Clause<'t> {
field: &'t FieldRef,
op: &'t OpKind,
literal: &'t LiteralVal,
}
fn eval_tokens(
tokens: &[Token],
action: &GovernanceAction,
agent_level: Option<GovernanceLevel>,
policy_ctx: Option<&dyn PolicyContext>,
) -> bool {
let mut or_groups: Vec<Vec<Clause>> = vec![Vec::new()];
let mut i = 0;
while i < tokens.len() {
match (&tokens[i], tokens.get(i + 1), tokens.get(i + 2)) {
(Token::Field(f), Some(Token::Op(op)), Some(Token::Literal(lit))) => {
let clause = Clause {
field: f,
op,
literal: lit,
};
or_groups.last_mut().unwrap().push(clause);
i += 3;
match tokens.get(i) {
None => break,
Some(Token::And) => {
i += 1; }
Some(Token::Or) => {
i += 1;
or_groups.push(Vec::new()); }
_ => return true, }
}
_ => return true, }
}
if or_groups.is_empty() || or_groups.iter().all(|g| g.is_empty()) {
return true;
}
or_groups.iter().any(|group| {
group
.iter()
.all(|c| eval_clause_safe(c.field, c.op, c.literal, action, agent_level, policy_ctx))
})
}
pub(crate) fn extract_field_names(expr: &str) -> Vec<String> {
const SKIP_WORDS: &[&str] = &[
"AND",
"OR",
"true",
"false",
"contains",
"starts_with",
"in",
"not_in",
"L0",
"L1",
"L2",
"L3",
"Low",
"Medium",
"High",
"Critical",
];
let mut names = Vec::new();
let mut chars = expr.chars().peekable();
while let Some(&ch) = chars.peek() {
if ch.is_whitespace() || matches!(ch, '<' | '>' | '=' | '!') {
chars.next();
continue;
}
if ch == '[' {
chars.next();
loop {
match chars.next() {
Some(']') | None => break,
_ => {}
}
}
continue;
}
if ch == '"' {
chars.next();
loop {
match chars.next() {
Some('"') | None => break,
Some('\\') => {
chars.next();
}
_ => {}
}
}
continue;
}
if ch.is_alphanumeric() || ch == '_' || ch == '-' || ch == '.' {
let mut word = String::new();
while let Some(&c) = chars.peek() {
if c.is_alphanumeric() || c == '_' || c == '-' || c == '.' {
word.push(c);
chars.next();
} else {
break;
}
}
if SKIP_WORDS.contains(&word.as_str()) || word.parse::<f64>().is_ok() {
continue;
}
names.push(word);
continue;
}
chars.next();
}
names
}
fn suggest_variable(name: &str) -> Option<&'static str> {
KNOWN_VARIABLES
.iter()
.copied()
.filter(|&v| strsim::levenshtein(name, v) <= 2)
.min_by_key(|&v| strsim::levenshtein(name, v))
}
pub(crate) fn validate_variables(expr: &str) -> Result<(), crate::policy::error::PolicyParseError> {
for name in extract_field_names(expr) {
if name.starts_with("args.") && name.len() > "args.".len() {
continue;
}
if name == "tool_result" || (name.starts_with("tool_result.") && name.len() > "tool_result.".len()) {
continue;
}
if !KNOWN_VARIABLES.contains(&name.as_str()) {
let suggestion = suggest_variable(&name).map(str::to_owned);
let available = KNOWN_VARIABLES.iter().map(|s| s.to_string()).collect();
return Err(crate::policy::error::PolicyParseError::UnknownVariable {
name,
suggestion,
available,
});
}
}
Ok(())
}
pub(crate) fn validate_governance_levels(expr: &str) -> Result<(), String> {
let mut chars = expr.chars().peekable();
while let Some(&ch) = chars.peek() {
if ch == 'L' {
let mut word = String::new();
while let Some(&c) = chars.peek() {
if c.is_alphanumeric() || c == '_' {
word.push(c);
chars.next();
} else {
break;
}
}
let rest = &word[1..];
if !rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit()) {
match word.as_str() {
"L0" | "L1" | "L2" | "L3" => {}
_ => {
return Err(format!(
"unknown governance level: {word}; valid values: L0, L1, L2, L3"
));
}
}
}
continue;
}
chars.next();
}
Ok(())
}
pub(crate) fn evaluate(
expr: &str,
action: &GovernanceAction,
agent_level: Option<GovernanceLevel>,
policy_ctx: Option<&dyn PolicyContext>,
) -> bool {
let tokens = match tokenize(expr) {
Some(t) if !t.is_empty() => t,
_ => return true, };
eval_tokens(&tokens, action, agent_level, policy_ctx)
}
#[cfg(test)]
mod tests {
use super::*;
use aa_core::{FileMode, GovernanceAction};
fn tool(name: &str) -> GovernanceAction {
GovernanceAction::ToolCall {
name: name.to_string(),
args: String::new(),
}
}
fn tool_with_args(name: &str, args: &str) -> GovernanceAction {
GovernanceAction::ToolCall {
name: name.to_string(),
args: args.to_string(),
}
}
fn file(path: &str) -> GovernanceAction {
GovernanceAction::FileAccess {
path: path.to_string(),
mode: FileMode::Read,
}
}
fn network(url: &str, method: &str) -> GovernanceAction {
GovernanceAction::NetworkRequest {
url: url.to_string(),
method: method.to_string(),
}
}
fn process(command: &str) -> GovernanceAction {
GovernanceAction::ProcessExec {
command: command.to_string(),
}
}
#[test]
fn eq_operator_matches_tool_name() {
assert!(evaluate(r#"tool == "search""#, &tool("search"), None, None));
}
#[test]
fn ne_operator_false_when_equal() {
assert!(!evaluate(r#"tool != "search""#, &tool("search"), None, None));
}
#[test]
fn contains_operator_on_url() {
assert!(evaluate(
r#"url contains "evil""#,
&network("https://evil.com", "GET"),
None,
None,
));
}
#[test]
fn starts_with_operator_on_path() {
assert!(evaluate(r#"path starts_with "/etc""#, &file("/etc/passwd"), None, None));
}
#[test]
fn and_combinator_all_true() {
assert!(evaluate(
r#"tool == "search" AND tool == "search""#,
&tool("search"),
None,
None,
));
}
#[test]
fn and_combinator_short_circuits() {
assert!(!evaluate(
r#"tool == "search" AND tool == "other""#,
&tool("search"),
None,
None,
));
}
#[test]
fn or_combinator_first_true() {
assert!(evaluate(
r#"tool == "x" OR tool == "search""#,
&tool("search"),
None,
None
));
}
#[test]
fn fail_safe_on_bad_expr() {
assert!(evaluate("not valid @@@ expr", &tool("anything"), None, None));
}
#[test]
fn field_absent_for_action_variant_returns_false() {
assert!(!evaluate(r#"tool == "foo""#, &process("ls"), None, None));
}
#[test]
fn rule_with_ge_l2_fires_for_l2_agent() {
assert!(evaluate(
"governance_level >= L2",
&tool("any"),
Some(GovernanceLevel::L2Enforce),
None,
));
}
#[test]
fn rule_with_ge_l2_does_not_fire_for_l1_agent() {
assert!(!evaluate(
"governance_level >= L2",
&tool("any"),
Some(GovernanceLevel::L1Observe),
None,
));
}
#[test]
fn rule_without_level_condition_fires_for_all_levels() {
for level in [
GovernanceLevel::L0Discover,
GovernanceLevel::L1Observe,
GovernanceLevel::L2Enforce,
GovernanceLevel::L3Native,
] {
assert!(
evaluate(r#"tool == "search""#, &tool("search"), Some(level), None),
"tool-only condition unexpectedly skipped for {level:?}"
);
}
}
fn fake_ctx(depth: Option<u32>) -> crate::policy::context::FakePolicyContext {
crate::policy::context::FakePolicyContext {
depth,
..Default::default()
}
}
#[test]
fn agent_depth_gt_matches_when_deeper() {
let ctx = fake_ctx(Some(3));
assert!(evaluate("agent.depth > 2", &tool("any"), None, Some(&ctx)));
}
#[test]
fn agent_depth_gt_no_match_when_shallower() {
let ctx = fake_ctx(Some(1));
assert!(!evaluate("agent.depth > 2", &tool("any"), None, Some(&ctx)));
}
#[test]
fn agent_depth_eq_matches_exact() {
let ctx = fake_ctx(Some(0));
assert!(evaluate("agent.depth == 0", &tool("any"), None, Some(&ctx)));
}
fn fake_team_ctx(active: Option<u64>) -> crate::policy::context::FakePolicyContext {
crate::policy::context::FakePolicyContext {
team_active: active,
..Default::default()
}
}
#[test]
fn team_active_agents_gt_matches() {
let ctx = fake_team_ctx(Some(6));
assert!(evaluate("team.active_agents > 5", &tool("any"), None, Some(&ctx)));
}
#[test]
fn team_active_agents_gt_no_match() {
let ctx = fake_team_ctx(Some(3));
assert!(!evaluate("team.active_agents > 5", &tool("any"), None, Some(&ctx)));
}
fn fake_budget_ctx(remaining: Option<f64>) -> crate::policy::context::FakePolicyContext {
crate::policy::context::FakePolicyContext {
team_budget: remaining,
..Default::default()
}
}
#[test]
fn team_budget_remaining_lt_matches_when_low() {
let ctx = fake_budget_ctx(Some(50.0));
assert!(evaluate("team.budget_remaining < 100", &tool("any"), None, Some(&ctx)));
}
#[test]
fn team_budget_remaining_lt_no_match_when_sufficient() {
let ctx = fake_budget_ctx(Some(200.0));
assert!(!evaluate("team.budget_remaining < 100", &tool("any"), None, Some(&ctx)));
}
fn fake_child_ctx(tools: Vec<&str>) -> crate::policy::context::FakePolicyContext {
crate::policy::context::FakePolicyContext {
child_tools: tools.into_iter().map(String::from).collect(),
..Default::default()
}
}
#[test]
fn child_tool_eq_matches_when_present() {
let ctx = fake_child_ctx(vec!["bash", "search"]);
assert!(evaluate(r#"child.tool == "bash""#, &tool("any"), None, Some(&ctx)));
}
#[test]
fn child_tool_eq_no_match_when_absent() {
let ctx = fake_child_ctx(vec!["search"]);
assert!(!evaluate(r#"child.tool == "bash""#, &tool("any"), None, Some(&ctx)));
}
#[test]
fn child_tool_ne_true_when_all_differ() {
let ctx = fake_child_ctx(vec!["search"]);
assert!(evaluate(r#"child.tool != "bash""#, &tool("any"), None, Some(&ctx)));
}
#[test]
fn null_safety_team_active_returns_false_when_no_team() {
let ctx = crate::policy::context::FakePolicyContext::default();
assert!(!evaluate("team.active_agents > 0", &tool("any"), None, Some(&ctx)));
}
#[test]
fn null_safety_returns_false_when_no_context() {
assert!(!evaluate("agent.depth > 0", &tool("any"), None, None));
}
fn fake_tier_ctx(
agent: Option<aa_core::RiskTier>,
parent: Option<aa_core::RiskTier>,
) -> crate::policy::context::FakePolicyContext {
crate::policy::context::FakePolicyContext {
agent_risk_tier: agent,
parent_risk_tier: parent,
..Default::default()
}
}
#[test]
fn agent_risk_tier_eq_matches_same_tier() {
let ctx = fake_tier_ctx(Some(aa_core::RiskTier::Medium), None);
assert!(evaluate("agent.risk_tier == Medium", &tool("any"), None, Some(&ctx)));
}
#[test]
fn agent_risk_tier_eq_no_match_different_tier() {
let ctx = fake_tier_ctx(Some(aa_core::RiskTier::Low), None);
assert!(!evaluate("agent.risk_tier == Medium", &tool("any"), None, Some(&ctx)));
}
#[test]
fn agent_risk_tier_gt_detects_escalation() {
let ctx = fake_tier_ctx(Some(aa_core::RiskTier::High), Some(aa_core::RiskTier::Medium));
assert!(evaluate("agent.risk_tier > Medium", &tool("any"), None, Some(&ctx)));
}
#[test]
fn parent_risk_tier_eq_matches() {
let ctx = fake_tier_ctx(Some(aa_core::RiskTier::High), Some(aa_core::RiskTier::Medium));
assert!(evaluate("parent.risk_tier == Medium", &tool("any"), None, Some(&ctx)));
}
#[test]
fn parent_risk_tier_returns_false_when_no_parent() {
let ctx = fake_tier_ctx(Some(aa_core::RiskTier::Low), None);
assert!(!evaluate("parent.risk_tier == Low", &tool("any"), None, Some(&ctx)));
}
#[test]
fn risk_tier_null_safe_no_context() {
assert!(!evaluate("agent.risk_tier == High", &tool("any"), None, None));
}
fn fake_child_tier_ctx(child: Option<aa_core::RiskTier>) -> crate::policy::context::FakePolicyContext {
crate::policy::context::FakePolicyContext {
child_risk_tier: child,
..Default::default()
}
}
#[test]
fn child_risk_tier_gt_denies_escalation() {
let ctx = fake_child_tier_ctx(Some(aa_core::RiskTier::High));
assert!(evaluate("child.risk_tier > Medium", &tool("any"), None, Some(&ctx)));
}
#[test]
fn child_risk_tier_same_tier_does_not_fire() {
let ctx = fake_child_tier_ctx(Some(aa_core::RiskTier::Medium));
assert!(!evaluate("child.risk_tier > Medium", &tool("any"), None, Some(&ctx)));
}
#[test]
fn child_risk_tier_eq_matches_exact() {
let ctx = fake_child_tier_ctx(Some(aa_core::RiskTier::Critical));
assert!(evaluate("child.risk_tier == Critical", &tool("any"), None, Some(&ctx)));
}
#[test]
fn child_risk_tier_null_safe_when_no_spawn_context() {
let ctx = fake_child_tier_ctx(None);
assert!(!evaluate("child.risk_tier > Low", &tool("any"), None, Some(&ctx)));
}
#[test]
fn child_risk_tier_null_safe_no_context_at_all() {
assert!(!evaluate("child.risk_tier == High", &tool("any"), None, None));
}
#[test]
fn validate_variables_accepts_known_variable() {
assert!(validate_variables("agent.depth > 2").is_ok());
assert!(validate_variables("team.active_agents == 5").is_ok());
assert!(validate_variables("child.tool == \"bash\"").is_ok());
assert!(validate_variables("child.risk_tier > Medium").is_ok());
assert!(validate_variables("child.risk_tier == Critical").is_ok());
}
#[test]
fn validate_variables_rejects_unknown_variable() {
let err = validate_variables("agent.xyz > 0").unwrap_err();
let msg = err.to_string();
assert!(msg.contains("agent.xyz"), "message should name the unknown var: {msg}");
assert!(msg.contains("agent.depth"), "message should list known vars: {msg}");
}
#[test]
fn validate_variables_suggests_typo_correction() {
let err = validate_variables("agent.depht > 0").unwrap_err();
match err {
crate::policy::error::PolicyParseError::UnknownVariable { name, suggestion, .. } => {
assert_eq!(name, "agent.depht");
assert_eq!(suggestion.as_deref(), Some("agent.depth"));
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn validate_variables_no_suggestion_when_too_different() {
let err = validate_variables("completely_unknown > 0").unwrap_err();
match err {
crate::policy::error::PolicyParseError::UnknownVariable { suggestion, .. } => {
assert!(
suggestion.is_none(),
"should not suggest a match for a very different name"
);
}
other => panic!("unexpected error: {other:?}"),
}
}
fn fake_age_ctx(age_secs: Option<u64>) -> crate::policy::context::FakePolicyContext {
crate::policy::context::FakePolicyContext {
agent_age_secs: age_secs,
..Default::default()
}
}
#[test]
fn agent_age_gt_24h_fires_when_agent_is_old() {
let ctx = fake_age_ctx(Some(90_000));
assert!(evaluate("agent.age > 24h", &tool("any"), None, Some(&ctx)));
}
#[test]
fn agent_age_gt_24h_no_match_when_agent_is_young() {
let ctx = fake_age_ctx(Some(36_000));
assert!(!evaluate("agent.age > 24h", &tool("any"), None, Some(&ctx)));
}
#[test]
fn team_parallel_agents_gt_matches() {
let ctx = crate::policy::context::FakePolicyContext {
team_active: Some(8),
..Default::default()
};
assert!(evaluate("team.parallel_agents > 5", &tool("any"), None, Some(&ctx)));
}
#[test]
fn null_safety_agent_age_returns_false_without_context() {
assert!(!evaluate("agent.age > 24h", &tool("any"), None, None));
}
fn send_message(source: Option<&str>, target: Option<&str>, channel: Option<&str>) -> GovernanceAction {
GovernanceAction::SendMessage {
source_team_id: source.map(String::from),
target_team_id: target.map(String::from),
channel_id: channel.map(String::from),
}
}
#[test]
fn source_team_id_eq_matches_same_team_message() {
let msg = send_message(Some("team-alpha"), Some("team-beta"), Some("ops"));
assert!(evaluate(r#"source.team_id == "team-alpha""#, &msg, None, None));
}
#[test]
fn target_team_id_and_channel_id_eq_match_cross_team_message() {
let msg = send_message(Some("team-alpha"), Some("team-beta"), Some("ops"));
assert!(evaluate(r#"target.team_id == "team-beta""#, &msg, None, None));
assert!(evaluate(r#"target.channel_id == "ops""#, &msg, None, None));
}
#[test]
fn channel_id_eq_no_match_when_different_channel() {
let msg = send_message(Some("team-alpha"), Some("team-beta"), Some("ops"));
assert!(!evaluate(r#"target.channel_id == "dev""#, &msg, None, None));
}
#[test]
fn null_safety_non_message_action_returns_false_for_message_fields() {
assert!(!evaluate(r#"source.team_id == "team-alpha""#, &tool("any"), None, None));
assert!(!evaluate(r#"target.team_id == "team-beta""#, &tool("any"), None, None));
assert!(!evaluate(r#"target.channel_id == "ops""#, &tool("any"), None, None));
}
#[test]
fn null_safety_none_fields_in_send_message_return_false() {
let msg = send_message(None, None, None);
assert!(!evaluate(r#"source.team_id == "team-alpha""#, &msg, None, None));
assert!(!evaluate(r#"target.team_id == "team-beta""#, &msg, None, None));
assert!(!evaluate(r#"target.channel_id == "ops""#, &msg, None, None));
}
#[test]
fn channel_id_in_list_matches_when_present() {
let msg = send_message(Some("team-alpha"), Some("team-beta"), Some("ops"));
assert!(evaluate(r#"target.channel_id in ["ops", "general"]"#, &msg, None, None));
}
#[test]
fn channel_id_in_list_no_match_when_absent() {
let msg = send_message(Some("team-alpha"), Some("team-beta"), Some("private"));
assert!(!evaluate(
r#"target.channel_id in ["ops", "general"]"#,
&msg,
None,
None
));
}
#[test]
fn channel_id_not_in_list_no_match_when_in_list() {
let msg = send_message(Some("team-alpha"), Some("team-beta"), Some("ops"));
assert!(!evaluate(
r#"target.channel_id not_in ["ops", "general"]"#,
&msg,
None,
None
));
}
#[test]
fn channel_id_not_in_list_matches_when_not_in_list() {
let msg = send_message(Some("team-alpha"), Some("team-beta"), Some("private"));
assert!(evaluate(
r#"target.channel_id not_in ["ops", "general"]"#,
&msg,
None,
None
));
}
#[test]
fn source_team_id_in_list_matches_known_team() {
let msg = send_message(Some("team-alpha"), Some("team-beta"), Some("ops"));
assert!(evaluate(
r#"source.team_id in ["team-alpha", "team-gamma"]"#,
&msg,
None,
None
));
}
#[test]
fn target_team_id_not_in_allows_non_restricted_team() {
let msg = send_message(Some("team-alpha"), Some("team-beta"), Some("ops"));
assert!(evaluate(
r#"target.team_id not_in ["team-restricted"]"#,
&msg,
None,
None
));
}
#[test]
fn in_operator_null_safe_for_non_message_action() {
assert!(!evaluate(r#"target.channel_id in ["ops"]"#, &tool("any"), None, None));
assert!(!evaluate(
r#"target.channel_id not_in ["ops"]"#,
&tool("any"),
None,
None
));
}
#[test]
fn in_list_with_empty_list_never_matches() {
let msg = send_message(Some("team-alpha"), Some("team-beta"), Some("ops"));
assert!(!evaluate(r#"target.channel_id in []"#, &msg, None, None));
assert!(evaluate(r#"target.channel_id not_in []"#, &msg, None, None));
}
#[test]
fn validate_variables_suggests_source_team_id_for_typo() {
let err = validate_variables(r#"source.team_d == "team-alpha""#).unwrap_err();
match err {
crate::policy::error::PolicyParseError::UnknownVariable { name, suggestion, .. } => {
assert_eq!(name, "source.team_d");
assert_eq!(suggestion.as_deref(), Some("source.team_id"));
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn parser_accepts_l0_through_l3() {
for (literal, level) in [
("L0", GovernanceLevel::L0Discover),
("L1", GovernanceLevel::L1Observe),
("L2", GovernanceLevel::L2Enforce),
("L3", GovernanceLevel::L3Native),
] {
let expr = format!("governance_level == {literal}");
assert!(
evaluate(&expr, &tool("any"), Some(level), None),
"{literal} did not parse / compare equal for matching agent level"
);
}
}
fn fake_topology_ctx(
parent_id: Option<&str>,
team_id: Option<&str>,
children_count: Option<u32>,
) -> crate::policy::context::FakePolicyContext {
crate::policy::context::FakePolicyContext {
agent_parent_id: parent_id.map(String::from),
agent_team_id: team_id.map(String::from),
agent_children_count: children_count,
..Default::default()
}
}
#[test]
fn agent_parent_id_eq_matches_known_parent() {
let ctx = fake_topology_ctx(Some("parent-abc"), None, None);
assert!(evaluate(
r#"agent.parent_agent_id == "parent-abc""#,
&tool("any"),
None,
Some(&ctx)
));
}
#[test]
fn agent_parent_id_eq_no_match_different_parent() {
let ctx = fake_topology_ctx(Some("parent-xyz"), None, None);
assert!(!evaluate(
r#"agent.parent_agent_id == "parent-abc""#,
&tool("any"),
None,
Some(&ctx)
));
}
#[test]
fn agent_parent_id_null_safe_when_no_parent() {
let ctx = fake_topology_ctx(None, None, None);
assert!(!evaluate(
r#"agent.parent_agent_id == "parent-abc""#,
&tool("any"),
None,
Some(&ctx)
));
}
#[test]
fn agent_team_id_eq_matches_known_team() {
let ctx = fake_topology_ctx(None, Some("team-alpha"), None);
assert!(evaluate(
r#"agent.team_id == "team-alpha""#,
&tool("any"),
None,
Some(&ctx)
));
}
#[test]
fn agent_team_id_eq_no_match_different_team() {
let ctx = fake_topology_ctx(None, Some("team-beta"), None);
assert!(!evaluate(
r#"agent.team_id == "team-alpha""#,
&tool("any"),
None,
Some(&ctx)
));
}
#[test]
fn agent_team_id_null_safe_when_no_team() {
let ctx = fake_topology_ctx(None, None, None);
assert!(!evaluate(
r#"agent.team_id == "team-alpha""#,
&tool("any"),
None,
Some(&ctx)
));
}
#[test]
fn agent_children_count_gt_matches_when_has_children() {
let ctx = fake_topology_ctx(None, None, Some(3));
assert!(evaluate("agent.children_count > 0", &tool("any"), None, Some(&ctx)));
}
#[test]
fn agent_children_count_eq_zero_matches_leaf() {
let ctx = fake_topology_ctx(None, None, Some(0));
assert!(evaluate("agent.children_count == 0", &tool("any"), None, Some(&ctx)));
}
#[test]
fn agent_children_count_null_safe_without_context() {
assert!(!evaluate("agent.children_count > 0", &tool("any"), None, None));
}
fn fake_depth_children_ctx(depth: Option<u32>, children: Option<u32>) -> crate::policy::context::FakePolicyContext {
crate::policy::context::FakePolicyContext {
depth,
agent_children_count: children,
..Default::default()
}
}
#[test]
fn agent_is_root_eq_1_matches_root_agent() {
let ctx = fake_depth_children_ctx(Some(0), None);
assert!(evaluate("agent.is_root == 1", &tool("any"), None, Some(&ctx)));
}
#[test]
fn agent_is_root_eq_1_no_match_non_root_agent() {
let ctx = fake_depth_children_ctx(Some(2), None);
assert!(!evaluate("agent.is_root == 1", &tool("any"), None, Some(&ctx)));
}
#[test]
fn agent_is_root_null_safe_without_context() {
assert!(!evaluate("agent.is_root == 1", &tool("any"), None, None));
}
#[test]
fn agent_is_leaf_eq_1_matches_agent_with_no_children() {
let ctx = fake_depth_children_ctx(None, Some(0));
assert!(evaluate("agent.is_leaf == 1", &tool("any"), None, Some(&ctx)));
}
#[test]
fn agent_is_leaf_eq_1_no_match_agent_with_children() {
let ctx = fake_depth_children_ctx(None, Some(2));
assert!(!evaluate("agent.is_leaf == 1", &tool("any"), None, Some(&ctx)));
}
#[test]
fn agent_is_leaf_null_safe_without_context() {
assert!(!evaluate("agent.is_leaf == 1", &tool("any"), None, None));
}
#[test]
fn validate_variables_accepts_new_topology_variables() {
assert!(validate_variables(r#"agent.parent_agent_id == "abc""#).is_ok());
assert!(validate_variables(r#"agent.team_id == "t1""#).is_ok());
assert!(validate_variables("agent.children_count > 0").is_ok());
assert!(validate_variables("agent.is_root == 1").is_ok());
assert!(validate_variables("agent.is_leaf == 1").is_ok());
}
fn fake_depth_budget_ctx(depth: Option<u32>, budget: Option<f64>) -> crate::policy::context::FakePolicyContext {
crate::policy::context::FakePolicyContext {
depth,
team_budget: budget,
..Default::default()
}
}
#[test]
fn compound_and_depth_budget_fires_when_both_clauses_true() {
let ctx = fake_depth_budget_ctx(Some(2), Some(50.0));
assert!(evaluate(
"agent.depth > 0 AND team.budget_remaining < 100",
&tool("any"),
None,
Some(&ctx),
));
}
#[test]
fn compound_and_depth_budget_no_fire_when_depth_clause_false() {
let ctx = fake_depth_budget_ctx(Some(0), Some(50.0));
assert!(!evaluate(
"agent.depth > 0 AND team.budget_remaining < 100",
&tool("any"),
None,
Some(&ctx),
));
}
#[test]
fn compound_and_depth_budget_no_fire_when_budget_clause_false() {
let ctx = fake_depth_budget_ctx(Some(2), Some(200.0));
assert!(!evaluate(
"agent.depth > 0 AND team.budget_remaining < 100",
&tool("any"),
None,
Some(&ctx),
));
}
#[test]
fn args_field_eq_matches_string_value() {
let action = tool_with_args("read_file", r#"{"path": "/etc/passwd"}"#);
assert!(evaluate(r#"args.path == "/etc/passwd""#, &action, None, None));
}
#[test]
fn args_starts_with_matches_etc_path_prefix() {
let action = tool_with_args("read_file", r#"{"path": "/etc/passwd"}"#);
assert!(evaluate(r#"args.path starts_with "/etc""#, &action, None, None));
}
#[test]
fn args_starts_with_no_match_outside_etc_prefix() {
let action = tool_with_args("read_file", r#"{"path": "/home/user/file.txt"}"#);
assert!(!evaluate(r#"args.path starts_with "/etc""#, &action, None, None));
}
#[test]
fn args_walks_nested_json_pointer() {
let action = tool_with_args("http_fetch", r#"{"headers": {"authorization": "Bearer abc"}}"#);
assert!(evaluate(
r#"args.headers.authorization starts_with "Bearer""#,
&action,
None,
None,
));
}
#[test]
fn args_missing_key_is_null_safe_no_match() {
let action = tool_with_args("read_file", r#"{"other": "value"}"#);
assert!(!evaluate(r#"args.path == "/etc/passwd""#, &action, None, None));
assert!(!evaluate(r#"args.path starts_with "/etc""#, &action, None, None));
}
#[test]
fn args_malformed_json_is_null_safe_no_match() {
let empty = tool_with_args("read_file", "");
assert!(!evaluate(r#"args.path == "/etc/passwd""#, &empty, None, None));
let garbage = tool_with_args("read_file", "{not valid json");
assert!(!evaluate(r#"args.path == "/etc/passwd""#, &garbage, None, None));
}
#[test]
fn args_in_list_matches_when_value_is_member() {
let action = tool_with_args("invoke", r#"{"action": "delete"}"#);
assert!(evaluate(
r#"args.action in ["delete", "drop", "truncate"]"#,
&action,
None,
None,
));
}
#[test]
fn args_not_in_list_matches_when_value_outside_allowlist() {
let action = tool_with_args("invoke", r#"{"action": "execute_bash"}"#);
assert!(evaluate(r#"args.action not_in ["read", "write"]"#, &action, None, None,));
}
#[test]
fn args_numeric_comparison_against_json_number() {
let action = tool_with_args("rpc_call", r#"{"timeout_ms": 30000}"#);
assert!(evaluate("args.timeout_ms > 1000", &action, None, None));
assert!(evaluate("args.timeout_ms <= 30000", &action, None, None));
assert!(!evaluate("args.timeout_ms < 100", &action, None, None));
}
#[test]
fn args_predicate_against_non_toolcall_action_is_no_match() {
assert!(!evaluate(
r#"args.path starts_with "/etc""#,
&file("/etc/passwd"),
None,
None
));
assert!(!evaluate(r#"args.path starts_with "/etc""#, &process("ls"), None, None));
assert!(!evaluate(
r#"args.path starts_with "/etc""#,
&network("https://example.com", "GET"),
None,
None,
));
}
fn tool_result_with_body(tool_name: &str, body: &str) -> GovernanceAction {
GovernanceAction::ToolResult {
tool_name: tool_name.to_string(),
result: body.to_string(),
}
}
#[test]
fn tool_result_field_eq_matches_string_leaf() {
let action = tool_result_with_body("read_file", r#"{"contents": "hello"}"#);
assert!(evaluate(r#"tool_result.contents == "hello""#, &action, None, None));
}
#[test]
fn tool_result_field_contains_matches_substring_in_leaf() {
let action = tool_result_with_body("read_file", r#"{"contents": "key=sk-abc123"}"#);
assert!(evaluate(r#"tool_result.contents contains "sk-""#, &action, None, None));
}
#[test]
fn tool_result_walks_nested_json_pointer() {
let action = tool_result_with_body("http_fetch", r#"{"payload": {"api_key": "sk-test-xyz"}}"#);
assert!(evaluate(
r#"tool_result.payload.api_key starts_with "sk-""#,
&action,
None,
None,
));
}
#[test]
fn bare_tool_result_contains_pattern_in_whole_body() {
let action = tool_result_with_body(
"search",
r#"{"items": [{"snippet": "leaked key: sk-test-123 in log"}]}"#,
);
assert!(evaluate(r#"tool_result contains "sk-""#, &action, None, None));
}
#[test]
fn bare_tool_result_starts_with_against_whole_body() {
let action = tool_result_with_body("ping", r#"{"ok": true}"#);
assert!(evaluate(r#"tool_result starts_with "{""#, &action, None, None));
}
#[test]
fn tool_result_missing_key_is_null_safe_no_match() {
let action = tool_result_with_body("read_file", r#"{"other": "value"}"#);
assert!(!evaluate(r#"tool_result.contents == "hello""#, &action, None, None,));
assert!(!evaluate(
r#"tool_result.contents starts_with "h""#,
&action,
None,
None,
));
}
#[test]
fn tool_result_malformed_json_is_null_safe_no_match() {
let empty = tool_result_with_body("read_file", "");
assert!(!evaluate(r#"tool_result.contents == "hello""#, &empty, None, None,));
let garbage = tool_result_with_body("read_file", "{not valid json");
assert!(!evaluate(r#"tool_result.contents == "hello""#, &garbage, None, None,));
assert!(evaluate(r#"tool_result contains "not valid""#, &garbage, None, None));
}
#[test]
fn tool_result_numeric_comparison_against_json_number() {
let action = tool_result_with_body("score", r#"{"score": 95}"#);
assert!(evaluate("tool_result.score > 90", &action, None, None));
assert!(evaluate("tool_result.score <= 95", &action, None, None));
assert!(!evaluate("tool_result.score < 50", &action, None, None));
assert!(evaluate("tool_result.score == 95", &action, None, None));
}
#[test]
fn tool_result_predicate_against_non_toolresult_action_is_no_match() {
assert!(!evaluate(
r#"tool_result.contents == "hello""#,
&tool("read_file"),
None,
None,
));
assert!(!evaluate(
r#"tool_result contains "sk-""#,
&file("/etc/passwd"),
None,
None
));
assert!(!evaluate(
r#"tool_result.api_key starts_with "sk-""#,
&network("https://example.com", "GET"),
None,
None,
));
assert!(!evaluate(r#"tool_result contains "x""#, &process("ls"), None, None));
}
}