use serde_json::Value;
use super::matcher;
use super::{ResidentBundle, Rule};
use crate::generated::types::{AgentFunction, PolicyRuleMode, PolicyRuleSeverity};
pub const EVENT_TYPE_PRE_TOOL_USE: &str = "pre_tool_use";
pub const SHELL_TOOL_NAMES: &[&str] = &["Bash"];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PolicyMatch {
pub rule_id: String,
pub reason: String,
pub severity: PolicyRuleSeverity,
pub mode: PolicyRuleMode,
pub shadow: bool,
}
pub fn normalize(cmd: &str) -> String {
cmd.split_whitespace().collect::<Vec<_>>().join(" ")
}
pub fn evaluate(
bundle: &ResidentBundle,
event_type: &str,
data: Option<&Value>,
) -> Option<PolicyMatch> {
if event_type != EVENT_TYPE_PRE_TOOL_USE {
return None;
}
let data = data?;
let tool_name = data.get("tool_name")?.as_str()?;
if !SHELL_TOOL_NAMES.contains(&tool_name) {
return None;
}
let command = data.get("tool_input")?.get("command")?.as_str()?;
evaluate_command(bundle, command)
}
pub fn evaluate_command(bundle: &ResidentBundle, command: &str) -> Option<PolicyMatch> {
let normalized = normalize(command);
let matched: Vec<&Rule> = bundle
.command_rules
.iter()
.filter(|r| {
scoped_matches(r, bundle.agent_function)
&& matcher::matches(&r.match_pattern, &normalized)
})
.collect();
if matched.is_empty() {
return None;
}
let deny =
bundle.enforcement_enabled && matched.iter().any(|r| r.mode == PolicyRuleMode::Enforce);
let deciding: Vec<&Rule> = if deny {
matched
.iter()
.copied()
.filter(|r| r.mode == PolicyRuleMode::Enforce)
.collect()
} else {
matched
};
let decider = deciding.iter().min_by_key(|r| r.rule_id.as_str())?;
Some(PolicyMatch {
rule_id: decider.rule_id.clone(),
reason: decider.reason.clone(),
severity: decider.severity,
mode: decider.mode,
shadow: !deny,
})
}
fn scoped_matches(rule: &Rule, agent_function: Option<AgentFunction>) -> bool {
rule.scoped_functions
.as_ref()
.is_none_or(|set| agent_function.is_some_and(|f| set.contains(&f)))
}
#[cfg(test)]
mod tests {
use std::time::{Duration, SystemTime};
use super::super::test_support::*;
use super::*;
use serde_json::json;
fn bash(command: &str) -> Value {
json!({ "tool_name": "Bash", "tool_input": { "command": command } })
}
fn enforce(rule_id: &str, pattern: &str) -> Rule {
rule(rule_id, pattern, PolicyRuleMode::Enforce)
}
fn observe(rule_id: &str, pattern: &str) -> Rule {
rule(rule_id, pattern, PolicyRuleMode::Observe)
}
#[test]
fn normalize_trims_and_collapses_only() {
assert_eq!(normalize(" rm -rf /tmp "), "rm -rf /tmp");
assert_eq!(normalize("rm\t-rf\n/tmp"), "rm -rf /tmp");
assert_eq!(normalize(""), "");
assert_eq!(normalize(" "), "");
assert_eq!(normalize("echo \"a b\" $HOME"), "echo \"a b\" $HOME");
}
#[test]
fn only_pre_tool_use_is_evaluated() {
let bundle = resident(vec![enforce("OL-CMD-001", "*rm -rf*")], true);
assert!(evaluate(&bundle, "post_tool_use", Some(&bash("rm -rf /"))).is_none());
assert!(evaluate(&bundle, "stop", Some(&bash("rm -rf /"))).is_none());
assert!(evaluate(&bundle, "pre_tool_use", Some(&bash("rm -rf /"))).is_some());
}
#[test]
fn tool_name_outside_the_frozen_list_is_never_evaluated() {
let bundle = resident(vec![enforce("OL-CMD-001", "*rm -rf*")], true);
for tool in [
"Read",
"Write",
"run_terminal_cmd", "shell", "run_shell_command",
"execute_command",
"bash", ] {
let data = json!({ "tool_name": tool, "tool_input": { "command": "rm -rf /" } });
assert!(
evaluate(&bundle, "pre_tool_use", Some(&data)).is_none(),
"{tool} must not be evaluated in v1"
);
}
}
#[test]
fn frozen_list_is_exactly_bash_in_v1() {
assert_eq!(SHELL_TOOL_NAMES, &["Bash"]);
}
#[test]
fn missing_or_non_string_command_allows() {
let bundle = resident(vec![enforce("OL-CMD-001", "*")], true);
assert!(evaluate(&bundle, "pre_tool_use", None).is_none());
assert!(evaluate(&bundle, "pre_tool_use", Some(&json!(null))).is_none());
assert!(evaluate(&bundle, "pre_tool_use", Some(&json!({}))).is_none());
assert!(evaluate(
&bundle,
"pre_tool_use",
Some(&json!({ "tool_name": "Bash" }))
)
.is_none());
assert!(evaluate(
&bundle,
"pre_tool_use",
Some(&json!({ "tool_name": "Bash", "tool_input": {} }))
)
.is_none());
assert!(evaluate(
&bundle,
"pre_tool_use",
Some(&json!({ "tool_name": "Bash", "tool_input": { "command": ["rm", "-rf", "/"] } }))
)
.is_none());
assert!(evaluate(
&bundle,
"pre_tool_use",
Some(&json!({ "tool_name": 42, "tool_input": { "command": "rm -rf /" } }))
)
.is_none());
}
#[test]
fn evaluate_uses_the_normalized_command() {
let bundle = resident(vec![enforce("OL-CMD-001", "rm -rf /tmp")], true);
let m = evaluate(&bundle, "pre_tool_use", Some(&bash(" rm -rf /tmp ")))
.expect("normalized command matches the anchored pattern");
assert_eq!(m.rule_id, "OL-CMD-001");
}
#[test]
fn empty_rule_set_allows() {
let bundle = resident(vec![], true);
assert!(evaluate(&bundle, "pre_tool_use", Some(&bash("rm -rf /"))).is_none());
}
#[test]
fn no_match_allows() {
let bundle = resident(vec![enforce("OL-CMD-001", "*rm -rf*")], true);
assert!(evaluate(&bundle, "pre_tool_use", Some(&bash("ls -la"))).is_none());
}
#[test]
fn enforce_match_denies_with_that_rules_id_and_reason() {
let bundle = resident(vec![enforce("OL-CMD-001", "*rm -rf*")], true);
let m = evaluate(&bundle, "pre_tool_use", Some(&bash("sudo rm -rf /tmp")))
.expect("rule matched");
assert!(
!m.shadow,
"an enforce match under an enabled kill switch denies"
);
assert_eq!(m.rule_id, "OL-CMD-001");
assert_eq!(m.reason, "OL-CMD-001 says no");
assert_eq!(m.mode, PolicyRuleMode::Enforce);
assert_eq!(m.severity, PolicyRuleSeverity::High);
}
#[test]
fn observe_match_allows_with_a_shadow_verdict() {
let bundle = resident(vec![observe("OL-CMD-001", "*rm -rf*")], true);
let m = evaluate(&bundle, "pre_tool_use", Some(&bash("rm -rf /tmp"))).expect("matched");
assert!(m.shadow, "observe never blocks");
assert_eq!(m.rule_id, "OL-CMD-001");
assert_eq!(m.mode, PolicyRuleMode::Observe);
}
#[test]
fn enforcement_disabled_forces_shadow() {
let bundle = resident(vec![enforce("OL-CMD-001", "*rm -rf*")], false);
let m = evaluate(&bundle, "pre_tool_use", Some(&bash("rm -rf /tmp"))).expect("matched");
assert!(m.shadow, "kill switch off => nothing blocks");
assert_eq!(m.rule_id, "OL-CMD-001");
assert_eq!(m.mode, PolicyRuleMode::Enforce);
}
#[test]
fn enforce_beats_observe_when_both_match() {
let bundle = resident(
vec![
observe("OL-CMD-AAA", "*rm*"),
enforce("OL-CMD-ZZZ", "*rm -rf*"),
],
true,
);
let m = evaluate(&bundle, "pre_tool_use", Some(&bash("rm -rf /tmp"))).expect("matched");
assert!(!m.shadow);
}
#[test]
fn reported_rule_comes_from_the_deciding_set_not_all_matches() {
let bundle = resident(
vec![
observe("OL-CMD-AAA", "*rm*"),
enforce("OL-CMD-ZZZ", "*rm -rf*"),
],
true,
);
let m = evaluate(&bundle, "pre_tool_use", Some(&bash("rm -rf /tmp"))).expect("matched");
assert_eq!(m.rule_id, "OL-CMD-ZZZ");
assert_eq!(m.reason, "OL-CMD-ZZZ says no");
assert!(!m.shadow);
}
#[test]
fn deciding_set_widens_when_enforcement_is_off() {
let bundle = resident(
vec![
observe("OL-CMD-AAA", "*rm*"),
enforce("OL-CMD-ZZZ", "*rm -rf*"),
],
false,
);
let m = evaluate(&bundle, "pre_tool_use", Some(&bash("rm -rf /tmp"))).expect("matched");
assert_eq!(m.rule_id, "OL-CMD-AAA");
assert!(m.shadow);
}
#[test]
fn lexicographically_first_enforce_rule_is_reported() {
let bundle = resident(
vec![
enforce("OL-CMD-ZZZ", "*rm -rf*"),
enforce("OL-CMD-BBB", "*rm*"),
enforce("OL-CMD-MMM", "*-rf*"),
],
true,
);
let m = evaluate(&bundle, "pre_tool_use", Some(&bash("rm -rf /tmp"))).expect("matched");
assert_eq!(m.rule_id, "OL-CMD-BBB");
}
#[test]
fn evaluation_is_order_independent() {
let rules = vec![
observe("OL-CMD-AAA", "*rm*"),
enforce("OL-CMD-ZZZ", "*rm -rf*"),
observe("OL-CMD-MMM", "*-rf*"),
enforce("OL-CMD-QQQ", "*rm -rf /tmp*"),
enforce("OL-CMD-DDD", "*/tmp*"),
];
let expected =
evaluate_command(&resident(rules.clone(), true), "rm -rf /tmp").expect("matched");
assert_eq!(expected.rule_id, "OL-CMD-DDD");
assert!(!expected.shadow);
let mut permuted = rules;
let len = permuted.len();
for i in 0..100 {
permuted.rotate_left(1 + (i % len));
if i % 3 == 0 {
permuted.reverse();
}
let got = evaluate_command(&resident(permuted.clone(), true), "rm -rf /tmp")
.expect("matched");
assert_eq!(got, expected, "permutation {i} changed the verdict");
}
}
#[test]
fn severity_and_reason_come_from_the_deciding_rule() {
let mut low = enforce("OL-CMD-BBB", "*rm*");
low.severity = PolicyRuleSeverity::Low;
low.reason = "low severity".to_string();
let mut critical = enforce("OL-CMD-ZZZ", "*rm*");
critical.severity = PolicyRuleSeverity::Critical;
let bundle = resident(vec![critical, low], true);
let m = evaluate_command(&bundle, "rm -rf /tmp").expect("matched");
assert_eq!(m.rule_id, "OL-CMD-BBB");
assert_eq!(m.severity, PolicyRuleSeverity::Low);
assert_eq!(m.reason, "low severity");
}
#[test]
fn matching_is_anchored_over_the_whole_command() {
let bundle = resident(vec![enforce("OL-CMD-001", "rm -rf /*")], true);
assert!(evaluate_command(&bundle, "rm -rf /tmp").is_some());
assert!(evaluate_command(&bundle, "sudo rm -rf /tmp").is_none());
}
const EVERY_FUNCTION: [AgentFunction; 14] = [
AgentFunction::Engineering,
AgentFunction::Product,
AgentFunction::Data,
AgentFunction::Security,
AgentFunction::ItOps,
AgentFunction::Sales,
AgentFunction::Marketing,
AgentFunction::Finance,
AgentFunction::Legal,
AgentFunction::Hr,
AgentFunction::Support,
AgentFunction::Research,
AgentFunction::Other,
AgentFunction::Unknown,
];
#[test]
fn every_function_covers_the_whole_vocabulary() {
let spelled: Vec<String> = EVERY_FUNCTION.iter().map(|f| f.to_string()).collect();
assert_eq!(spelled, super::super::validate::agent_function_vocabulary());
}
fn scoped(mut r: Rule, functions: &[AgentFunction]) -> Rule {
r.scoped_functions = Some(functions.to_vec());
r
}
fn every_context() -> impl Iterator<Item = Option<AgentFunction>> {
EVERY_FUNCTION.into_iter().map(Some).chain([None])
}
fn resident_for(function: Option<AgentFunction>, rules: Vec<Rule>) -> ResidentBundle {
let mut bundle = resident(rules, true);
bundle.agent_function = function;
bundle
}
#[test]
fn agent_context_rule_matching() {
let rules = vec![scoped(
enforce("OL-CMD-002", "*psql*"),
&[AgentFunction::Marketing, AgentFunction::Sales],
)];
let marketing = resident_for(Some(AgentFunction::Marketing), rules.clone());
let m = evaluate(&marketing, "pre_tool_use", Some(&bash("psql -h prod")))
.expect("a member function matches");
assert_eq!(m.rule_id, "OL-CMD-002");
assert!(!m.shadow, "enforce + member function => deny");
let sales = resident_for(Some(AgentFunction::Sales), rules.clone());
assert!(evaluate_command(&sales, "psql -h prod").is_some_and(|m| !m.shadow));
let engineering = resident_for(Some(AgentFunction::Engineering), rules);
assert!(evaluate_command(&engineering, "psql -h prod").is_none());
}
#[test]
fn agent_context_stale_still_matches() {
let rules = vec![scoped(
enforce("OL-CMD-002", "*psql*"),
&[AgentFunction::Marketing],
)];
let fresh = resident_for(Some(AgentFunction::Marketing), rules.clone());
let mut stale = resident_for(Some(AgentFunction::Marketing), rules);
stale.built_at = SystemTime::UNIX_EPOCH;
stale.revision = 1;
let now = fresh.built_at + Duration::from_secs(60);
assert!(
stale.age_seconds(now) > fresh.age_seconds(now),
"the fixture is genuinely stale"
);
assert_eq!(
evaluate_command(&stale, "psql -h prod"),
evaluate_command(&fresh, "psql -h prod"),
"bundle age is invisible to evaluation"
);
assert!(evaluate_command(&stale, "psql -h prod").is_some_and(|m| !m.shadow));
}
#[test]
fn agent_context_unknown_not_wildcard() {
let only_unknown = vec![scoped(
enforce("OL-CMD-003", "*curl*"),
&[AgentFunction::Unknown],
)];
assert!(evaluate_command(
&resident_for(Some(AgentFunction::Unknown), only_unknown.clone()),
"curl https://x"
)
.is_some());
assert!(evaluate_command(
&resident_for(Some(AgentFunction::Marketing), only_unknown.clone()),
"curl https://x"
)
.is_none());
assert!(
evaluate_command(&resident_for(None, only_unknown), "curl https://x").is_none(),
"absent context is not `unknown`"
);
let marketing_only = vec![scoped(
enforce("OL-CMD-002", "*psql*"),
&[AgentFunction::Marketing],
)];
assert!(evaluate_command(
&resident_for(Some(AgentFunction::Unknown), marketing_only),
"psql -h prod"
)
.is_none());
}
#[test]
fn absent_context_means_scoped_rules_match_nothing() {
let bundle = resident_for(
None,
vec![
scoped(enforce("OL-CMD-AAA", "*rm -rf*"), &EVERY_FUNCTION),
enforce("OL-CMD-ZZZ", "*rm -rf*"),
],
);
let m = evaluate_command(&bundle, "rm -rf /tmp").expect("the unconditional rule matches");
assert_eq!(
m.rule_id, "OL-CMD-ZZZ",
"the scoped rule is not in the deciding set even though it sorts first"
);
assert!(!m.shadow);
let only_scoped = resident_for(
None,
vec![scoped(enforce("OL-CMD-AAA", "*rm -rf*"), &EVERY_FUNCTION)],
);
assert!(
evaluate_command(&only_scoped, "rm -rf /tmp").is_none(),
"with only scoped rules the command is allowed outright"
);
}
#[test]
fn conditions_and_semantics_all_must_hold() {
let both = scoped_wire_rule(
"OL-CMD-002",
"*psql*",
vec![
function_in(&[AgentFunction::Marketing, AgentFunction::Sales]),
function_in(&[AgentFunction::Sales, AgentFunction::Finance]),
],
);
let disjoint = scoped_wire_rule(
"OL-CMD-004",
"*psql*",
vec![
function_in(&[AgentFunction::Marketing]),
function_in(&[AgentFunction::Finance]),
],
);
let for_function = |f: AgentFunction| {
let mut resident = ResidentBundle::from_bundle(&wire_bundle(
vec![both.clone(), disjoint.clone()],
true,
));
assert_eq!(resident.command_rules.len(), 2, "both rules load");
resident.agent_function = Some(f);
resident
};
let m = evaluate_command(&for_function(AgentFunction::Sales), "psql -h prod")
.expect("sales is in both sets");
assert_eq!(m.rule_id, "OL-CMD-002", "the disjoint rule admits nobody");
assert!(!m.shadow);
for outside in [
AgentFunction::Marketing,
AgentFunction::Finance,
AgentFunction::Unknown,
] {
assert!(
evaluate_command(&for_function(outside), "psql -h prod").is_none(),
"{outside} is in at most one of the sets"
);
}
}
#[test]
fn a_rule_without_conditions_is_unconditional() {
let rules = vec![enforce("OL-CMD-001", "*rm -rf*")];
for function in every_context() {
let m = evaluate_command(&resident_for(function, rules.clone()), "rm -rf /tmp")
.unwrap_or_else(|| panic!("{function:?} matches an unscoped rule"));
assert_eq!(m.rule_id, "OL-CMD-001");
assert!(!m.shadow);
}
}
#[test]
fn a_scoped_out_rule_leaves_the_deciding_set_untouched() {
let rules = vec![
scoped(enforce("OL-CMD-AAA", "*rm*"), &[AgentFunction::Finance]),
observe("OL-CMD-MMM", "*rm*"),
enforce("OL-CMD-ZZZ", "*rm -rf*"),
];
let m = evaluate_command(
&resident_for(Some(AgentFunction::Engineering), rules.clone()),
"rm -rf /tmp",
)
.expect("matched");
assert_eq!(m.rule_id, "OL-CMD-ZZZ");
assert!(!m.shadow);
let m = evaluate_command(
&resident_for(Some(AgentFunction::Finance), rules.clone()),
"rm -rf /tmp",
)
.expect("matched");
assert_eq!(m.rule_id, "OL-CMD-AAA");
assert!(!m.shadow);
let mut off = resident_for(Some(AgentFunction::Engineering), rules);
off.enforcement_enabled = false;
let m = evaluate_command(&off, "rm -rf /tmp").expect("matched");
assert_eq!(m.rule_id, "OL-CMD-MMM");
assert!(m.shadow);
}
#[test]
fn an_empty_scope_matches_no_install() {
let rules = vec![scoped(enforce("OL-CMD-004", "*"), &[])];
for function in every_context() {
assert!(
evaluate_command(&resident_for(function, rules.clone()), "anything").is_none(),
"{function:?} must not be admitted by an empty scope"
);
}
}
}