use crate::generated::types::{PolicyBundleEffectClasses, T1Leaf};
use super::super::facts::FactSet;
use super::super::types::{Classification, Event};
use super::attrs_of;
pub fn entries_for<'a>(
feed: Option<&'a PolicyBundleEffectClasses>,
tool_name: &str,
) -> Vec<&'a crate::generated::types::EffectClassEntry> {
feed.map(|feed| {
feed.entries
.iter()
.filter(|entry| entry.tool_key.as_deref() == Some(tool_name))
.collect()
})
.unwrap_or_default()
}
pub fn apply(
entries: &[&crate::generated::types::EffectClassEntry],
event: &Event,
cls: &mut Classification,
facts: &FactSet,
) -> bool {
let mut matched = false;
for entry in entries {
if !entry
.selectors
.iter()
.all(|leaf| selector_matches(leaf, event, cls, facts))
{
continue;
}
matched = true;
for tuple in &entry.effects {
let verb = tuple
.verb
.as_ref()
.map(|v| v.0.as_str())
.unwrap_or("unknown");
let target = tuple
.target_class
.as_ref()
.map(|t| t.0.as_str())
.unwrap_or("shell");
super::add_effect(cls, verb, target, attrs_of(&[]));
}
}
matched
}
fn selector_matches(leaf: &T1Leaf, event: &Event, cls: &Classification, _facts: &FactSet) -> bool {
let Some(field) = leaf.field.as_deref() else {
return false;
};
let pred = leaf.pred.as_deref().unwrap_or("");
let values = field_values(field, event, cls);
match pred {
"exists" => !values.is_empty(),
"equals" => leaf
.value
.as_ref()
.and_then(|v| v.as_str())
.is_some_and(|want| values.iter().any(|got| got == want)),
"in_set" => leaf
.value
.as_ref()
.and_then(|v| v.as_array())
.is_some_and(|wanted| {
wanted
.iter()
.filter_map(|w| w.as_str())
.any(|want| values.iter().any(|got| got == want))
}),
"keyword" => leaf
.pattern
.as_deref()
.is_some_and(|needle| values.iter().any(|got| got.contains(needle))),
"prefix" => leaf
.pattern
.as_deref()
.is_some_and(|needle| values.iter().any(|got| got.starts_with(needle))),
"glob" => leaf.pattern.as_deref().is_some_and(|pattern| {
values
.iter()
.any(|got| super::path::glob_match(pattern, got))
}),
_ => false,
}
}
fn field_values(field: &str, event: &Event, cls: &Classification) -> Vec<String> {
if field == "tool.name" {
return if event.tool_name.is_empty() {
Vec::new()
} else {
vec![event.tool_name.clone()]
};
}
if field == "input.strings" {
let mut out = Vec::new();
collect_strings(&event.tool_input, &mut out);
return out;
}
if let Some(pointer) = field.strip_prefix("input.") {
return json_pointer(&event.tool_input, pointer)
.map(|value| match value {
serde_json::Value::String(text) => vec![text.clone()],
serde_json::Value::Null => Vec::new(),
other => vec![other.to_string()],
})
.unwrap_or_default();
}
if field == "path.class" {
return cls.paths.iter().map(|p| p.class.0.clone()).collect();
}
if field == "path.value" {
return cls.paths.iter().map(|p| p.value.clone()).collect();
}
Vec::new()
}
fn json_pointer<'a>(
document: &'a serde_json::Value,
pointer: &str,
) -> Option<&'a serde_json::Value> {
if pointer.is_empty() || pointer == "/" {
return Some(document);
}
document.pointer(pointer)
}
fn collect_strings(value: &serde_json::Value, out: &mut Vec<String>) {
match value {
serde_json::Value::String(text) => out.push(text.clone()),
serde_json::Value::Array(items) => items.iter().for_each(|i| collect_strings(i, out)),
serde_json::Value::Object(map) => map.values().for_each(|v| collect_strings(v, out)),
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn feed(json: serde_json::Value) -> PolicyBundleEffectClasses {
serde_json::from_value(json).expect("the fixture feed parses")
}
fn event(tool: &str, input: serde_json::Value) -> Event {
Event {
tool_name: tool.to_string(),
tool_input: input,
..Event::default()
}
}
#[test]
fn an_entry_with_no_selectors_classifies_every_call_of_the_tool() {
let feed = feed(serde_json::json!({"entries": [{
"tool_key": "mcp__s3__delete_object",
"selectors": [],
"effects": [{"verb": "delete", "target_class": "data_store"}],
}]}));
let event = event("mcp__s3__delete_object", serde_json::json!({"Key": "x"}));
let mut cls = Classification::default();
let entries = entries_for(Some(&feed), &event.tool_name);
assert!(apply(&entries, &event, &mut cls, &FactSet::default()));
assert_eq!(cls.effects.len(), 1);
}
#[test]
fn a_selector_miss_is_silence_and_never_a_wholesale_erasure() {
let feed = feed(serde_json::json!({"entries": [{
"tool_key": "mcp__s3__put_bucket_lifecycle_configuration",
"selectors": [{"pred": "exists", "field": "input./Bucket"}],
"effects": [{"verb": "delete", "target_class": "data_store"}],
}]}));
let event = event(
"mcp__s3__put_bucket_lifecycle_configuration",
serde_json::json!({"LifecycleConfiguration": {"Rules": []}}),
);
let mut cls = Classification::default();
let entries = entries_for(Some(&feed), &event.tool_name);
assert!(
!apply(&entries, &event, &mut cls, &FactSet::default()),
"the entry does not claim the tool, so the caller still falls back"
);
assert!(cls.effects.is_empty());
}
}