Skip to main content

polyhook_core/
tools.rs

1use crate::types::CallerKind;
2
3/// Normalize a vendor-specific tool name to the canonical polyhook tool name.
4///
5/// The lookup is case-insensitive. If no mapping exists the original name is
6/// returned unchanged.
7pub fn normalize_tool(vendor: &str, caller: &CallerKind) -> String {
8    let lower = vendor.to_lowercase();
9
10    let canonical = match caller {
11        CallerKind::ClaudeCode => normalize_claude_code(&lower),
12        CallerKind::Cursor => normalize_cursor(&lower),
13        CallerKind::Windsurf => normalize_windsurf(&lower),
14        CallerKind::Cline => normalize_cline(&lower),
15        CallerKind::Amp => normalize_amp(&lower),
16        CallerKind::Unknown => None,
17    };
18
19    canonical
20        .map(|s| s.to_owned())
21        .unwrap_or_else(|| vendor.to_owned())
22}
23
24fn normalize_claude_code(lower: &str) -> Option<&'static str> {
25    match lower {
26        "bash" => Some("bash"),
27        "read" => Some("read_file"),
28        "write" => Some("write_file"),
29        "edit" => Some("edit_file"),
30        "ls" => Some("list_dir"),
31        "grep" => Some("grep"),
32        "glob" => Some("glob"),
33        "websearch" => Some("web_search"),
34        "webfetch" => Some("web_fetch"),
35        "task" => Some("spawn_agent"),
36        "mcp__ide__getdiagnostics" => Some("diagnostics"),
37        _ => None,
38    }
39}
40
41fn normalize_cursor(lower: &str) -> Option<&'static str> {
42    match lower {
43        "run_terminal_cmd" => Some("bash"),
44        "read_file" => Some("read_file"),
45        "edit_file" => Some("write_file"),
46        "apply_edit" => Some("edit_file"),
47        "list_dir" => Some("list_dir"),
48        "grep_search" => Some("grep"),
49        "file_search" => Some("glob"),
50        "web_search" => Some("web_search"),
51        "fetch_url" => Some("web_fetch"),
52        "spawn_agent" => Some("spawn_agent"),
53        "get_diagnostics" => Some("diagnostics"),
54        "move_file" => Some("move_file"),
55        "delete_file" => Some("delete_file"),
56        "create_dir" => Some("create_dir"),
57        _ => None,
58    }
59}
60
61fn normalize_windsurf(lower: &str) -> Option<&'static str> {
62    match lower {
63        "run_command" => Some("bash"),
64        "read_file" => Some("read_file"),
65        "write_file" => Some("write_file"),
66        "edit_file" => Some("edit_file"),
67        "list_directory" => Some("list_dir"),
68        "search_files" => Some("grep"),
69        "find_files" => Some("glob"),
70        "search_web" => Some("web_search"),
71        "fetch_page" => Some("web_fetch"),
72        "spawn_agent" => Some("spawn_agent"),
73        "get_diagnostics" => Some("diagnostics"),
74        "move_file" => Some("move_file"),
75        "delete_file" => Some("delete_file"),
76        "create_directory" => Some("create_dir"),
77        _ => None,
78    }
79}
80
81fn normalize_cline(lower: &str) -> Option<&'static str> {
82    match lower {
83        "execute_command" => Some("bash"),
84        "read_file" => Some("read_file"),
85        "write_to_file" => Some("write_file"),
86        "apply_diff" => Some("edit_file"),
87        "list_files" => Some("list_dir"),
88        "search_files" => Some("grep"),
89        "search" => Some("web_search"),
90        "fetch" => Some("web_fetch"),
91        "rename_file" => Some("move_file"),
92        "delete_file" => Some("delete_file"),
93        "create_directory" => Some("create_dir"),
94        "get_diagnostics" => Some("diagnostics"),
95        _ => None,
96    }
97}
98
99fn normalize_amp(lower: &str) -> Option<&'static str> {
100    match lower {
101        "shell" => Some("bash"),
102        "file.read" => Some("read_file"),
103        "file.write" => Some("write_file"),
104        "file.edit" => Some("edit_file"),
105        "fs.list" => Some("list_dir"),
106        "search.grep" => Some("grep"),
107        "search.glob" => Some("glob"),
108        "web.search" => Some("web_search"),
109        "web.fetch" => Some("web_fetch"),
110        "agent.spawn" => Some("spawn_agent"),
111        "lsp.diagnostics" => Some("diagnostics"),
112        "fs.move" => Some("move_file"),
113        "fs.delete" => Some("delete_file"),
114        "fs.mkdir" => Some("create_dir"),
115        _ => None,
116    }
117}
118
119// ---------------------------------------------------------------------------
120// Tests
121// ---------------------------------------------------------------------------
122
123#[cfg(test)]
124mod tests {
125    use super::normalize_tool;
126    use crate::CallerKind;
127
128    #[test]
129    fn unknown_caller_returns_original() {
130        assert_eq!(normalize_tool("SomeTool", &CallerKind::Unknown), "SomeTool");
131        assert_eq!(normalize_tool("bash", &CallerKind::Unknown), "bash");
132        assert_eq!(normalize_tool("Bash", &CallerKind::Unknown), "Bash");
133    }
134
135    // ClaudeCode extra mappings
136    #[test] fn claude_ls()          { assert_eq!(normalize_tool("ls",                         &CallerKind::ClaudeCode), "list_dir"); }
137    #[test] fn claude_grep()        { assert_eq!(normalize_tool("grep",                       &CallerKind::ClaudeCode), "grep"); }
138    #[test] fn claude_glob()        { assert_eq!(normalize_tool("glob",                       &CallerKind::ClaudeCode), "glob"); }
139    #[test] fn claude_websearch()   { assert_eq!(normalize_tool("websearch",                  &CallerKind::ClaudeCode), "web_search"); }
140    #[test] fn claude_webfetch()    { assert_eq!(normalize_tool("webfetch",                   &CallerKind::ClaudeCode), "web_fetch"); }
141    #[test] fn claude_mcp_diag()    { assert_eq!(normalize_tool("mcp__ide__getdiagnostics",   &CallerKind::ClaudeCode), "diagnostics"); }
142
143    // Cursor extra mappings
144    #[test] fn cursor_apply_edit()     { assert_eq!(normalize_tool("apply_edit",     &CallerKind::Cursor), "edit_file"); }
145    #[test] fn cursor_list_dir()       { assert_eq!(normalize_tool("list_dir",       &CallerKind::Cursor), "list_dir"); }
146    #[test] fn cursor_file_search()    { assert_eq!(normalize_tool("file_search",    &CallerKind::Cursor), "glob"); }
147    #[test] fn cursor_fetch_url()      { assert_eq!(normalize_tool("fetch_url",      &CallerKind::Cursor), "web_fetch"); }
148    #[test] fn cursor_spawn_agent()    { assert_eq!(normalize_tool("spawn_agent",    &CallerKind::Cursor), "spawn_agent"); }
149    #[test] fn cursor_get_diag()       { assert_eq!(normalize_tool("get_diagnostics",&CallerKind::Cursor), "diagnostics"); }
150    #[test] fn cursor_move_file()      { assert_eq!(normalize_tool("move_file",      &CallerKind::Cursor), "move_file"); }
151    #[test] fn cursor_delete_file()    { assert_eq!(normalize_tool("delete_file",    &CallerKind::Cursor), "delete_file"); }
152    #[test] fn cursor_create_dir()     { assert_eq!(normalize_tool("create_dir",     &CallerKind::Cursor), "create_dir"); }
153
154    // Windsurf extra mappings
155    #[test] fn windsurf_edit_file()    { assert_eq!(normalize_tool("edit_file",       &CallerKind::Windsurf), "edit_file"); }
156    #[test] fn windsurf_search_files() { assert_eq!(normalize_tool("search_files",    &CallerKind::Windsurf), "grep"); }
157    #[test] fn windsurf_find_files()   { assert_eq!(normalize_tool("find_files",      &CallerKind::Windsurf), "glob"); }
158    #[test] fn windsurf_fetch_page()   { assert_eq!(normalize_tool("fetch_page",      &CallerKind::Windsurf), "web_fetch"); }
159    #[test] fn windsurf_spawn_agent()  { assert_eq!(normalize_tool("spawn_agent",     &CallerKind::Windsurf), "spawn_agent"); }
160    #[test] fn windsurf_get_diag()     { assert_eq!(normalize_tool("get_diagnostics", &CallerKind::Windsurf), "diagnostics"); }
161    #[test] fn windsurf_move_file()    { assert_eq!(normalize_tool("move_file",       &CallerKind::Windsurf), "move_file"); }
162    #[test] fn windsurf_delete_file()  { assert_eq!(normalize_tool("delete_file",     &CallerKind::Windsurf), "delete_file"); }
163    #[test] fn windsurf_create_dir()   { assert_eq!(normalize_tool("create_directory",&CallerKind::Windsurf), "create_dir"); }
164
165    // Cline extra mappings
166    #[test] fn cline_search_files(){ assert_eq!(normalize_tool("search_files",      &CallerKind::Cline), "grep"); }
167    #[test] fn cline_search()      { assert_eq!(normalize_tool("search",           &CallerKind::Cline), "web_search"); }
168    #[test] fn cline_fetch()       { assert_eq!(normalize_tool("fetch",            &CallerKind::Cline), "web_fetch"); }
169    #[test] fn cline_rename_file() { assert_eq!(normalize_tool("rename_file",      &CallerKind::Cline), "move_file"); }
170    #[test] fn cline_delete_file() { assert_eq!(normalize_tool("delete_file",      &CallerKind::Cline), "delete_file"); }
171    #[test] fn cline_create_dir()  { assert_eq!(normalize_tool("create_directory", &CallerKind::Cline), "create_dir"); }
172    #[test] fn cline_get_diag()    { assert_eq!(normalize_tool("get_diagnostics",  &CallerKind::Cline), "diagnostics"); }
173
174    // Amp extra mappings
175    #[test] fn amp_search_grep()   { assert_eq!(normalize_tool("search.grep",    &CallerKind::Amp), "grep"); }
176    #[test] fn amp_search_glob()   { assert_eq!(normalize_tool("search.glob",    &CallerKind::Amp), "glob"); }
177    #[test] fn amp_web_fetch()     { assert_eq!(normalize_tool("web.fetch",      &CallerKind::Amp), "web_fetch"); }
178    #[test] fn amp_agent_spawn()   { assert_eq!(normalize_tool("agent.spawn",    &CallerKind::Amp), "spawn_agent"); }
179    #[test] fn amp_lsp_diag()      { assert_eq!(normalize_tool("lsp.diagnostics",&CallerKind::Amp), "diagnostics"); }
180    #[test] fn amp_fs_move()       { assert_eq!(normalize_tool("fs.move",        &CallerKind::Amp), "move_file"); }
181    #[test] fn amp_fs_delete()     { assert_eq!(normalize_tool("fs.delete",      &CallerKind::Amp), "delete_file"); }
182    #[test] fn amp_fs_mkdir()      { assert_eq!(normalize_tool("fs.mkdir",       &CallerKind::Amp), "create_dir"); }
183
184    // Fallthrough: unmapped tool names pass through unchanged for each known caller
185    #[test] fn claude_unknown_falls_through()   { assert_eq!(normalize_tool("no_such_tool", &CallerKind::ClaudeCode), "no_such_tool"); }
186    #[test] fn cursor_unknown_falls_through()   { assert_eq!(normalize_tool("no_such_tool", &CallerKind::Cursor),    "no_such_tool"); }
187    #[test] fn windsurf_unknown_falls_through() { assert_eq!(normalize_tool("no_such_tool", &CallerKind::Windsurf),  "no_such_tool"); }
188    #[test] fn cline_unknown_falls_through()    { assert_eq!(normalize_tool("no_such_tool", &CallerKind::Cline),     "no_such_tool"); }
189    #[test] fn amp_unknown_falls_through()      { assert_eq!(normalize_tool("no_such_tool", &CallerKind::Amp),       "no_such_tool"); }
190}