use crate::claude::tools as ct;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EffectOption {
Allow,
Deny,
Ask,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ObservableTag {
ToolName,
HookType,
AgentName,
PositionalArg,
HasArg,
NamedArg,
NestedField,
FsOp,
FsPath,
NetDomain,
}
pub struct ToolTuiInfo {
pub def: &'static ct::ToolDef,
pub allowed_effects: &'static [EffectOption],
pub relevant_observables: &'static [ObservableTag],
}
const ALL_EFFECTS: &[EffectOption] = &[EffectOption::Allow, EffectOption::Deny, EffectOption::Ask];
const NO_ASK: &[EffectOption] = &[EffectOption::Allow, EffectOption::Deny];
pub const TOOLS: &[ToolTuiInfo] = &[
ToolTuiInfo {
def: &ct::BASH,
allowed_effects: NO_ASK,
relevant_observables: &[
ObservableTag::PositionalArg,
ObservableTag::HasArg,
ObservableTag::NamedArg,
],
},
ToolTuiInfo {
def: &ct::READ,
allowed_effects: ALL_EFFECTS,
relevant_observables: &[ObservableTag::FsPath, ObservableTag::FsOp],
},
ToolTuiInfo {
def: &ct::WRITE,
allowed_effects: ALL_EFFECTS,
relevant_observables: &[ObservableTag::FsPath, ObservableTag::FsOp],
},
ToolTuiInfo {
def: &ct::EDIT,
allowed_effects: ALL_EFFECTS,
relevant_observables: &[ObservableTag::FsPath, ObservableTag::FsOp],
},
ToolTuiInfo {
def: &ct::MULTI_EDIT,
allowed_effects: ALL_EFFECTS,
relevant_observables: &[ObservableTag::FsPath, ObservableTag::FsOp],
},
ToolTuiInfo {
def: &ct::GLOB,
allowed_effects: ALL_EFFECTS,
relevant_observables: &[ObservableTag::FsPath, ObservableTag::FsOp],
},
ToolTuiInfo {
def: &ct::GREP,
allowed_effects: ALL_EFFECTS,
relevant_observables: &[ObservableTag::FsPath, ObservableTag::FsOp],
},
ToolTuiInfo {
def: &ct::WEB_FETCH,
allowed_effects: ALL_EFFECTS,
relevant_observables: &[ObservableTag::NetDomain],
},
ToolTuiInfo {
def: &ct::WEB_SEARCH,
allowed_effects: ALL_EFFECTS,
relevant_observables: &[ObservableTag::NetDomain],
},
ToolTuiInfo {
def: &ct::AGENT,
allowed_effects: ALL_EFFECTS,
relevant_observables: &[ObservableTag::AgentName, ObservableTag::NamedArg],
},
ToolTuiInfo {
def: &ct::NOTEBOOK_EDIT,
allowed_effects: ALL_EFFECTS,
relevant_observables: &[ObservableTag::FsPath],
},
];
pub fn lookup(name: &str) -> Option<&'static ToolTuiInfo> {
TOOLS.iter().find(|t| t.def.name.eq_ignore_ascii_case(name))
}
pub fn is_effect_allowed(tool_name: &str, effect: EffectOption) -> bool {
match lookup(tool_name) {
Some(info) => info.allowed_effects.contains(&effect),
None => true,
}
}
pub fn is_observable_relevant(tool_name: &str, tag: ObservableTag) -> bool {
match lookup(tool_name) {
Some(info) => info.relevant_observables.contains(&tag),
None => true,
}
}
pub fn effect_options_for_tool(tool_name: Option<&str>) -> (Vec<String>, Vec<&'static str>) {
let all = [
(EffectOption::Allow, "allow (permit)", ""),
(EffectOption::Deny, "deny (block)", ""),
(EffectOption::Ask, "ask (prompt)", ""),
];
match tool_name.and_then(lookup) {
Some(info) => {
let mut labels = Vec::new();
let mut hints = Vec::new();
for &(effect, label, hint) in &all {
if info.allowed_effects.contains(&effect) {
labels.push(label.into());
hints.push(hint);
}
}
(labels, hints)
}
None => {
let labels = all.iter().map(|(_, l, _)| l.to_string()).collect();
let hints = all.iter().map(|(_, _, h)| *h).collect();
(labels, hints)
}
}
}
pub fn filtered_effect_to_canonical(tool_name: Option<&str>, filtered_idx: usize) -> usize {
let all = [EffectOption::Allow, EffectOption::Deny, EffectOption::Ask];
match tool_name.and_then(lookup) {
Some(info) => {
let allowed: Vec<usize> = all
.iter()
.enumerate()
.filter(|(_, e)| info.allowed_effects.contains(e))
.map(|(i, _)| i)
.collect();
allowed.get(filtered_idx).copied().unwrap_or(filtered_idx)
}
None => filtered_idx,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bash_no_ask() {
assert!(is_effect_allowed("Bash", EffectOption::Allow));
assert!(is_effect_allowed("Bash", EffectOption::Deny));
assert!(!is_effect_allowed("Bash", EffectOption::Ask));
}
#[test]
fn test_case_insensitive_lookup() {
assert!(lookup("bash").is_some());
assert!(lookup("BASH").is_some());
assert!(lookup("Bash").is_some());
}
#[test]
fn test_unknown_tool_permissive() {
assert!(is_effect_allowed("mcp__custom_tool", EffectOption::Ask));
assert!(is_observable_relevant(
"mcp__custom_tool",
ObservableTag::FsPath
));
}
#[test]
fn test_effect_filtering() {
let (labels, _) = effect_options_for_tool(Some("Bash"));
assert_eq!(labels.len(), 2);
assert!(labels.contains(&"allow (permit)".to_string()));
assert!(labels.contains(&"deny (block)".to_string()));
assert!(!labels.contains(&"ask (prompt)".to_string()));
}
#[test]
fn test_filtered_index_mapping() {
assert_eq!(filtered_effect_to_canonical(Some("Bash"), 0), 0); assert_eq!(filtered_effect_to_canonical(Some("Bash"), 1), 1); assert_eq!(filtered_effect_to_canonical(None, 2), 2);
}
#[test]
fn test_observable_relevance() {
assert!(is_observable_relevant("Bash", ObservableTag::PositionalArg));
assert!(is_observable_relevant("Bash", ObservableTag::HasArg));
assert!(!is_observable_relevant("Bash", ObservableTag::FsPath));
assert!(!is_observable_relevant("Bash", ObservableTag::NetDomain));
assert!(is_observable_relevant("Read", ObservableTag::FsPath));
assert!(!is_observable_relevant("Read", ObservableTag::NetDomain));
assert!(is_observable_relevant(
"WebSearch",
ObservableTag::NetDomain
));
assert!(!is_observable_relevant("WebSearch", ObservableTag::FsPath));
}
#[test]
fn tui_tools_reference_canonical_defs() {
for tui_info in TOOLS {
assert!(
ct::lookup(tui_info.def.name).is_some(),
"TUI tool {:?} not found in canonical registry",
tui_info.def.name
);
}
}
}