use std::collections::HashMap;
use std::fs;
use std::path::Path;
pub fn build_command_key(tool_name: &str, args: &serde_json::Value) -> String {
match tool_name {
"sh" => {
if let Some(cmd) = args.get("bash_command").and_then(|v| v.as_str()) {
let parts: Vec<&str> = cmd.trim().split_whitespace().collect();
if parts.is_empty() {
return "sh".to_string();
}
let base = format!("sh:{}", parts[0]);
if parts.len() > 1 && !parts[1].starts_with('-') {
return format!("{} {}", base, parts[1]);
}
return base;
}
"sh".to_string()
}
"python" => "python".to_string(),
"edit_file" => {
if let Some(fp) = args.get("filepath").and_then(|v| v.as_str()) {
let basename = Path::new(fp)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(fp);
return format!("edit_file:{}", basename);
}
"edit_file".to_string()
}
"delegate" => {
if let Some(target) = args.get("target").and_then(|v| v.as_str()) {
return format!("delegate:{}", target);
}
"delegate".to_string()
}
other => other.to_string(),
}
}
pub fn match_permission(cmd_key: &str, rules: &HashMap<String, String>) -> Option<String> {
if let Some(v) = rules.get(cmd_key) {
return Some(v.clone());
}
let mut best: Option<&str> = None;
let mut best_len = 0usize;
for rule_key in rules.keys() {
if cmd_key.starts_with(rule_key.as_str()) {
let next = cmd_key.as_bytes().get(rule_key.len()).copied();
let at_boundary = matches!(next, None | Some(b':') | Some(b' '));
if at_boundary && rule_key.len() > best_len {
best_len = rule_key.len();
best = Some(rule_key.as_str());
}
}
}
best.and_then(|k| rules.get(k)).cloned()
}
pub fn load_permission_file(path: &str) -> HashMap<String, String> {
let content = match fs::read_to_string(path) {
Ok(s) => s,
Err(_) => return HashMap::new(),
};
let parsed: serde_yaml::Value = match serde_yaml::from_str(&content) {
Ok(v) => v,
Err(_) => return HashMap::new(),
};
let map = match &parsed {
serde_yaml::Value::Mapping(m) => {
let inner = m.get("rules").and_then(|v| v.as_mapping()).unwrap_or(m);
inner
}
_ => return HashMap::new(),
};
map.iter()
.filter_map(|(k, v)| {
let key = k.as_str()?.to_string();
let val = match v {
serde_yaml::Value::String(s) => s.clone(),
serde_yaml::Value::Bool(b) => b.to_string(),
_ => v.as_str()?.to_string(),
};
Some((key, val))
})
.collect()
}
pub fn is_safe_tool(name: &str) -> bool {
matches!(
name,
"chat"
| "help"
| "stop"
| "screenshot"
| "ask_form"
| "config"
| "switches"
| "verbose"
| "shh"
| "usage"
| "lookback"
| "reload"
| "init"
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_command_key_sh() {
let args = serde_json::json!({"bash_command": "git commit -m 'foo'"});
assert_eq!(build_command_key("sh", &args), "sh:git commit");
}
#[test]
fn test_build_command_key_sh_single() {
let args = serde_json::json!({"bash_command": "ls -la"});
assert_eq!(build_command_key("sh", &args), "sh:ls");
}
#[test]
fn test_build_command_key_edit_file() {
let args = serde_json::json!({"filepath": "src/main.rs"});
assert_eq!(build_command_key("edit_file", &args), "edit_file:main.rs");
}
#[test]
fn test_match_permission_exact() {
let mut rules = HashMap::new();
rules.insert("sh:git commit".to_string(), "auto".to_string());
assert_eq!(
match_permission("sh:git commit", &rules).as_deref(),
Some("auto")
);
}
#[test]
fn test_match_permission_prefix() {
let mut rules = HashMap::new();
rules.insert("sh:git".to_string(), "ask".to_string());
rules.insert("sh".to_string(), "deny".to_string());
assert_eq!(
match_permission("sh:git commit -m foo", &rules).as_deref(),
Some("ask")
);
}
#[test]
fn test_match_permission_no_false_prefix() {
let mut rules = HashMap::new();
rules.insert("sh:git".to_string(), "auto".to_string());
assert_eq!(match_permission("sh:gitstatus", &rules), None);
}
}