use vigil_types::{EffectKind, EffectVector, ToolInvocation};
use crate::extract::EffectExtractor;
struct CatalogEntry {
server_hints: &'static [&'static str],
tool: &'static str,
effects: &'static [EffectKind],
destructive: bool,
}
use EffectKind::{CommSend, DbRead, FsRead, FsWrite, NetOutbound, SecretUse};
const fn e(
server_hints: &'static [&'static str],
tool: &'static str,
effects: &'static [EffectKind],
) -> CatalogEntry {
CatalogEntry {
server_hints,
tool,
effects,
destructive: false,
}
}
static CATALOG: &[CatalogEntry] = &[
e(&["filesystem", "file", "fs"], "read_file", &[FsRead]),
e(&["filesystem", "file", "fs"], "read_text_file", &[FsRead]),
e(&["filesystem", "file", "fs"], "read_media_file", &[FsRead]),
e(
&["filesystem", "file", "fs"],
"read_multiple_files",
&[FsRead],
),
e(&["filesystem", "file", "fs"], "list_directory", &[FsRead]),
e(
&["filesystem", "file", "fs"],
"list_directory_with_sizes",
&[FsRead],
),
e(&["filesystem", "file", "fs"], "directory_tree", &[FsRead]),
e(&["filesystem", "file", "fs"], "search_files", &[FsRead]),
e(&["filesystem", "file", "fs"], "get_file_info", &[FsRead]),
e(&["filesystem", "file", "fs"], "write_file", &[FsWrite]),
e(&["filesystem", "file", "fs"], "edit_file", &[FsWrite]),
e(
&["filesystem", "file", "fs"],
"create_directory",
&[FsWrite],
),
e(&["filesystem", "file", "fs"], "move_file", &[FsWrite]),
e(&["fetch", "http", "web"], "fetch", &[NetOutbound]),
e(&["git"], "git_status", &[FsRead]),
e(&["git"], "git_log", &[FsRead]),
e(&["git"], "git_diff", &[FsRead]),
e(&["git"], "git_show", &[FsRead]),
e(&["git"], "git_add", &[FsWrite]),
e(&["git"], "git_commit", &[FsWrite]),
e(
&["github"],
"search_repositories",
&[NetOutbound, SecretUse],
),
e(&["github"], "get_file_contents", &[NetOutbound, SecretUse]),
e(&["github"], "get_issue", &[NetOutbound, SecretUse]),
e(&["github"], "list_issues", &[NetOutbound, SecretUse]),
e(
&["github"],
"create_issue",
&[NetOutbound, SecretUse, CommSend],
),
e(
&["github"],
"create_pull_request",
&[NetOutbound, SecretUse, CommSend],
),
e(
&["github"],
"add_issue_comment",
&[NetOutbound, SecretUse, CommSend],
),
e(
&["github"],
"create_or_update_file",
&[NetOutbound, SecretUse, CommSend],
),
e(
&["github"],
"push_files",
&[NetOutbound, SecretUse, CommSend],
),
e(&["brave"], "brave_web_search", &[NetOutbound, SecretUse]),
e(&["brave"], "brave_local_search", &[NetOutbound, SecretUse]),
e(
&["slack"],
"slack_post_message",
&[NetOutbound, SecretUse, CommSend],
),
e(
&["slack"],
"slack_reply_to_thread",
&[NetOutbound, SecretUse, CommSend],
),
e(&["postgres", "postgresql", "pg"], "query", &[DbRead]),
];
#[derive(Debug, Default)]
pub struct CatalogExtractor;
impl CatalogExtractor {
pub fn new() -> Self {
Self
}
}
impl EffectExtractor for CatalogExtractor {
fn name(&self) -> &'static str {
"catalog"
}
fn extract(&self, call: &ToolInvocation, out: &mut EffectVector) {
let server_lc = call.server_id.to_ascii_lowercase();
for entry in CATALOG {
if entry.tool != call.tool_name {
continue;
}
let server_ok = entry.server_hints.is_empty()
|| entry.server_hints.iter().any(|h| server_lc.contains(h));
if !server_ok {
continue;
}
for &eff in entry.effects {
if !out.effects.contains(&eff) {
out.effects.push(eff);
}
}
if entry.destructive {
out.destructive = true;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn call(server_id: &str, tool_name: &str) -> ToolInvocation {
ToolInvocation {
invocation_id: "i".into(),
session_id: "s".into(),
server_id: server_id.into(),
tool_name: tool_name.into(),
args: json!({}),
descriptor_hash: "h".into(),
requested_at: 0,
}
}
fn effects_of(server_id: &str, tool_name: &str) -> EffectVector {
let mut ev = EffectVector::default();
CatalogExtractor::new().extract(&call(server_id, tool_name), &mut ev);
ev
}
#[test]
fn filesystem_read_write_classified() {
assert_eq!(effects_of("filesystem", "read_file").effects, vec![FsRead]);
assert_eq!(effects_of("my-fs", "write_file").effects, vec![FsWrite]);
assert_eq!(effects_of("files", "edit_file").effects, vec![FsWrite]);
}
#[test]
fn github_write_is_net_secret_comm() {
let ev = effects_of("github", "create_issue");
assert!(ev.effects.contains(&NetOutbound));
assert!(ev.effects.contains(&SecretUse));
assert!(ev.effects.contains(&CommSend));
let r = effects_of("github", "get_issue");
assert!(r.effects.contains(&NetOutbound) && !r.effects.contains(&CommSend));
}
#[test]
fn unknown_tool_or_server_no_effect() {
assert!(effects_of("filesystem", "totally_unknown_tool")
.effects
.is_empty());
assert!(effects_of("weather-api", "write_file").effects.is_empty());
}
#[test]
fn monotonic_append_only_never_clears() {
let mut ev = EffectVector {
effects: vec![EffectKind::SecretUse],
paths_write: vec!["/x".into()],
destructive: true,
..Default::default()
};
CatalogExtractor::new().extract(&call("filesystem", "write_file"), &mut ev);
assert!(ev.effects.contains(&EffectKind::SecretUse)); assert!(ev.effects.contains(&FsWrite)); assert_eq!(ev.paths_write, vec!["/x".to_string()]); assert!(ev.destructive); }
#[test]
fn dedup_within_extractor() {
let mut ev = EffectVector {
effects: vec![FsRead],
..Default::default()
};
CatalogExtractor::new().extract(&call("filesystem", "read_file"), &mut ev);
assert_eq!(ev.effects, vec![FsRead]);
}
}