use meerkat_core::tool_scope::ToolFilter;
use meerkat_core::types::ToolDef;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::sync::Arc;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParentToolScopeSnapshot {
pub visible_tool_names: HashSet<String>,
pub visible_tool_defs: Vec<ToolDef>,
pub captured_at: chrono::DateTime<chrono::Utc>,
}
impl ParentToolScopeSnapshot {
pub fn to_tool_filter(&self) -> ToolFilter {
ToolFilter::Allow(self.visible_tool_names.clone())
}
pub fn with_overlays(
&self,
allow_overlay: Option<&HashSet<String>>,
deny_overlay: Option<&HashSet<String>>,
) -> ToolFilter {
let mut names = self.visible_tool_names.clone();
if let Some(allow) = allow_overlay {
names = names.intersection(allow).cloned().collect();
}
if let Some(deny) = deny_overlay {
for name in deny {
names.remove(name);
}
}
ToolFilter::Allow(names)
}
pub fn from_tools(tools: &[Arc<ToolDef>]) -> Self {
Self {
visible_tool_names: tools.iter().map(|t| t.name.clone()).collect(),
visible_tool_defs: tools.iter().map(|t| (**t).clone()).collect(),
captured_at: chrono::Utc::now(),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use serde_json;
use std::collections::HashSet;
fn make_tool(name: &str) -> Arc<ToolDef> {
Arc::new(ToolDef {
name: name.to_string(),
description: format!("{name} tool"),
input_schema: serde_json::json!({"type": "object"}),
provenance: None,
})
}
#[test]
fn roundtrip_serialization() {
let tools = vec![make_tool("read"), make_tool("write")];
let snapshot = ParentToolScopeSnapshot::from_tools(&tools);
let json = serde_json::to_string(&snapshot).unwrap();
let parsed: ParentToolScopeSnapshot = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.visible_tool_names, snapshot.visible_tool_names);
assert_eq!(parsed.visible_tool_defs.len(), 2);
}
#[test]
fn to_tool_filter_produces_allow() {
let tools = vec![make_tool("a"), make_tool("b")];
let snapshot = ParentToolScopeSnapshot::from_tools(&tools);
let filter = snapshot.to_tool_filter();
match filter {
ToolFilter::Allow(names) => {
assert!(names.contains("a"));
assert!(names.contains("b"));
assert_eq!(names.len(), 2);
}
other => panic!("expected Allow, got {other:?}"),
}
}
#[test]
fn with_overlays_intersects_allow() {
let tools = vec![make_tool("a"), make_tool("b"), make_tool("c")];
let snapshot = ParentToolScopeSnapshot::from_tools(&tools);
let allow: HashSet<String> = ["b", "c", "d"].iter().copied().map(String::from).collect();
let filter = snapshot.with_overlays(Some(&allow), None);
match filter {
ToolFilter::Allow(names) => {
assert!(names.contains("b"));
assert!(names.contains("c"));
assert!(!names.contains("a")); assert!(!names.contains("d")); }
other => panic!("expected Allow, got {other:?}"),
}
}
#[test]
fn with_overlays_removes_deny() {
let tools = vec![make_tool("a"), make_tool("b"), make_tool("c")];
let snapshot = ParentToolScopeSnapshot::from_tools(&tools);
let deny: HashSet<String> = ["b"].iter().copied().map(String::from).collect();
let filter = snapshot.with_overlays(None, Some(&deny));
match filter {
ToolFilter::Allow(names) => {
assert!(names.contains("a"));
assert!(names.contains("c"));
assert!(!names.contains("b"));
}
other => panic!("expected Allow, got {other:?}"),
}
}
#[test]
fn with_overlays_both() {
let tools = vec![make_tool("a"), make_tool("b"), make_tool("c")];
let snapshot = ParentToolScopeSnapshot::from_tools(&tools);
let allow: HashSet<String> = ["a", "b"].iter().copied().map(String::from).collect();
let deny: HashSet<String> = ["b"].iter().copied().map(String::from).collect();
let filter = snapshot.with_overlays(Some(&allow), Some(&deny));
match filter {
ToolFilter::Allow(names) => {
assert!(names.contains("a"));
assert!(!names.contains("b"));
assert!(!names.contains("c"));
assert_eq!(names.len(), 1);
}
other => panic!("expected Allow, got {other:?}"),
}
}
#[test]
fn from_tools_captures_names_and_defs() {
let tools = vec![make_tool("x"), make_tool("y")];
let snapshot = ParentToolScopeSnapshot::from_tools(&tools);
assert_eq!(snapshot.visible_tool_names.len(), 2);
assert_eq!(snapshot.visible_tool_defs.len(), 2);
assert!(snapshot.visible_tool_names.contains("x"));
assert!(snapshot.visible_tool_names.contains("y"));
}
}