Skip to main content

punch_runtime/
tools.rs

1//! Built-in tool definitions (JSON schemas for the LLM).
2//!
3//! This module defines the tool schemas that get sent to the LLM so it knows
4//! what tools are available. The actual execution logic lives in `tool_executor`.
5
6use punch_types::{Capability, ToolCategory, ToolDefinition};
7
8/// Return all built-in tool definitions that match the given capabilities.
9///
10/// Only tools the fighter is allowed to use (based on granted capabilities) are
11/// included. This prevents the LLM from seeing tools it can't invoke.
12pub fn tools_for_capabilities(capabilities: &[Capability]) -> Vec<ToolDefinition> {
13    let mut tools = Vec::new();
14
15    for cap in capabilities {
16        match cap {
17            Capability::FileRead(_) => {
18                push_unique(&mut tools, file_read());
19                push_unique(&mut tools, file_list());
20                push_unique(&mut tools, file_search());
21                push_unique(&mut tools, file_info());
22            }
23            Capability::FileWrite(_) => {
24                push_unique(&mut tools, file_write());
25                push_unique(&mut tools, patch_apply());
26            }
27            Capability::ShellExec(_) => {
28                push_unique(&mut tools, shell_exec());
29                push_unique(&mut tools, process_list());
30                push_unique(&mut tools, process_kill());
31                push_unique(&mut tools, env_get());
32                push_unique(&mut tools, env_list());
33            }
34            Capability::Network(_) => {
35                push_unique(&mut tools, web_fetch());
36                push_unique(&mut tools, web_search());
37                push_unique(&mut tools, http_request());
38                push_unique(&mut tools, http_post());
39            }
40            Capability::Memory => {
41                push_unique(&mut tools, memory_store());
42                push_unique(&mut tools, memory_recall());
43            }
44            Capability::KnowledgeGraph => {
45                push_unique(&mut tools, knowledge_add_entity());
46                push_unique(&mut tools, knowledge_add_relation());
47                push_unique(&mut tools, knowledge_query());
48            }
49            Capability::AgentSpawn => {
50                push_unique(&mut tools, agent_spawn());
51            }
52            Capability::AgentMessage => {
53                push_unique(&mut tools, agent_message());
54                push_unique(&mut tools, agent_list());
55            }
56            Capability::BrowserControl => {
57                push_unique(&mut tools, browser_navigate());
58                push_unique(&mut tools, browser_screenshot());
59                push_unique(&mut tools, browser_click());
60                push_unique(&mut tools, browser_type());
61                push_unique(&mut tools, browser_content());
62            }
63            Capability::SourceControl => {
64                push_unique(&mut tools, git_status());
65                push_unique(&mut tools, git_diff());
66                push_unique(&mut tools, git_log());
67                push_unique(&mut tools, git_commit());
68                push_unique(&mut tools, git_branch());
69            }
70            Capability::Container => {
71                push_unique(&mut tools, docker_ps());
72                push_unique(&mut tools, docker_run());
73                push_unique(&mut tools, docker_build());
74                push_unique(&mut tools, docker_logs());
75            }
76            Capability::DataManipulation => {
77                push_unique(&mut tools, json_query());
78                push_unique(&mut tools, json_transform());
79                push_unique(&mut tools, yaml_parse());
80                push_unique(&mut tools, regex_match());
81                push_unique(&mut tools, regex_replace());
82                push_unique(&mut tools, text_diff());
83                push_unique(&mut tools, text_count());
84            }
85            Capability::Schedule => {
86                push_unique(&mut tools, schedule_task());
87                push_unique(&mut tools, schedule_list());
88                push_unique(&mut tools, schedule_cancel());
89            }
90            Capability::CodeAnalysis => {
91                push_unique(&mut tools, code_search());
92                push_unique(&mut tools, code_symbols());
93            }
94            Capability::Archive => {
95                push_unique(&mut tools, archive_create());
96                push_unique(&mut tools, archive_extract());
97                push_unique(&mut tools, archive_list());
98            }
99            Capability::Template => {
100                push_unique(&mut tools, template_render());
101            }
102            Capability::Crypto => {
103                push_unique(&mut tools, hash_compute());
104                push_unique(&mut tools, hash_verify());
105            }
106            _ => {}
107        }
108    }
109
110    tools
111}
112
113/// Return ALL built-in tool definitions (for unrestricted fighters).
114pub fn all_tools() -> Vec<ToolDefinition> {
115    vec![
116        file_read(),
117        file_write(),
118        file_list(),
119        patch_apply(),
120        shell_exec(),
121        web_fetch(),
122        web_search(),
123        memory_store(),
124        memory_recall(),
125        knowledge_add_entity(),
126        knowledge_add_relation(),
127        knowledge_query(),
128        agent_spawn(),
129        agent_message(),
130        agent_list(),
131        browser_navigate(),
132        browser_screenshot(),
133        browser_click(),
134        browser_type(),
135        browser_content(),
136        // Git / Source Control
137        git_status(),
138        git_diff(),
139        git_log(),
140        git_commit(),
141        git_branch(),
142        // Container
143        docker_ps(),
144        docker_run(),
145        docker_build(),
146        docker_logs(),
147        // HTTP
148        http_request(),
149        http_post(),
150        // Data manipulation
151        json_query(),
152        json_transform(),
153        yaml_parse(),
154        regex_match(),
155        regex_replace(),
156        // Process
157        process_list(),
158        process_kill(),
159        // Schedule
160        schedule_task(),
161        schedule_list(),
162        schedule_cancel(),
163        // Code analysis
164        code_search(),
165        code_symbols(),
166        // Archive
167        archive_create(),
168        archive_extract(),
169        archive_list(),
170        // Template
171        template_render(),
172        // Crypto / Hash
173        hash_compute(),
174        hash_verify(),
175        // Environment
176        env_get(),
177        env_list(),
178        // Text
179        text_diff(),
180        text_count(),
181        // File (extended)
182        file_search(),
183        file_info(),
184    ]
185}
186
187fn push_unique(tools: &mut Vec<ToolDefinition>, tool: ToolDefinition) {
188    if !tools.iter().any(|t| t.name == tool.name) {
189        tools.push(tool);
190    }
191}
192
193// ---------------------------------------------------------------------------
194// Tool definitions
195// ---------------------------------------------------------------------------
196
197fn file_read() -> ToolDefinition {
198    ToolDefinition {
199        name: "file_read".into(),
200        description: "Read the contents of a file at the given path.".into(),
201        input_schema: serde_json::json!({
202            "type": "object",
203            "properties": {
204                "path": {
205                    "type": "string",
206                    "description": "The file path to read (relative to working directory or absolute)."
207                }
208            },
209            "required": ["path"]
210        }),
211        category: ToolCategory::FileSystem,
212    }
213}
214
215fn file_write() -> ToolDefinition {
216    ToolDefinition {
217        name: "file_write".into(),
218        description:
219            "Write content to a file at the given path. Creates parent directories if needed."
220                .into(),
221        input_schema: serde_json::json!({
222            "type": "object",
223            "properties": {
224                "path": {
225                    "type": "string",
226                    "description": "The file path to write to."
227                },
228                "content": {
229                    "type": "string",
230                    "description": "The content to write to the file."
231                }
232            },
233            "required": ["path", "content"]
234        }),
235        category: ToolCategory::FileSystem,
236    }
237}
238
239fn file_list() -> ToolDefinition {
240    ToolDefinition {
241        name: "file_list".into(),
242        description: "List files and directories at the given path.".into(),
243        input_schema: serde_json::json!({
244            "type": "object",
245            "properties": {
246                "path": {
247                    "type": "string",
248                    "description": "The directory path to list (defaults to working directory)."
249                }
250            }
251        }),
252        category: ToolCategory::FileSystem,
253    }
254}
255
256fn shell_exec() -> ToolDefinition {
257    ToolDefinition {
258        name: "shell_exec".into(),
259        description: "Execute a shell command and return stdout, stderr, and exit code.".into(),
260        input_schema: serde_json::json!({
261            "type": "object",
262            "properties": {
263                "command": {
264                    "type": "string",
265                    "description": "The shell command to execute."
266                }
267            },
268            "required": ["command"]
269        }),
270        category: ToolCategory::Shell,
271    }
272}
273
274fn web_fetch() -> ToolDefinition {
275    ToolDefinition {
276        name: "web_fetch".into(),
277        description: "Fetch the content of a URL via HTTP GET.".into(),
278        input_schema: serde_json::json!({
279            "type": "object",
280            "properties": {
281                "url": {
282                    "type": "string",
283                    "description": "The URL to fetch."
284                }
285            },
286            "required": ["url"]
287        }),
288        category: ToolCategory::Web,
289    }
290}
291
292fn web_search() -> ToolDefinition {
293    ToolDefinition {
294        name: "web_search".into(),
295        description:
296            "Search the web using DuckDuckGo and return the top results with titles and URLs."
297                .into(),
298        input_schema: serde_json::json!({
299            "type": "object",
300            "properties": {
301                "query": {
302                    "type": "string",
303                    "description": "The search query."
304                }
305            },
306            "required": ["query"]
307        }),
308        category: ToolCategory::Web,
309    }
310}
311
312fn memory_store() -> ToolDefinition {
313    ToolDefinition {
314        name: "memory_store".into(),
315        description: "Store a key-value pair in your persistent memory. Use this to remember important facts, user preferences, or context across conversations.".into(),
316        input_schema: serde_json::json!({
317            "type": "object",
318            "properties": {
319                "key": {
320                    "type": "string",
321                    "description": "A short descriptive key for the memory."
322                },
323                "value": {
324                    "type": "string",
325                    "description": "The value to remember."
326                },
327                "confidence": {
328                    "type": "number",
329                    "description": "Confidence level from 0.0 to 1.0 (default: 0.9)."
330                }
331            },
332            "required": ["key", "value"]
333        }),
334        category: ToolCategory::Memory,
335    }
336}
337
338fn memory_recall() -> ToolDefinition {
339    ToolDefinition {
340        name: "memory_recall".into(),
341        description: "Search your persistent memory for previously stored information.".into(),
342        input_schema: serde_json::json!({
343            "type": "object",
344            "properties": {
345                "query": {
346                    "type": "string",
347                    "description": "Search query to find relevant memories."
348                },
349                "limit": {
350                    "type": "integer",
351                    "description": "Maximum number of results (default: 10)."
352                }
353            },
354            "required": ["query"]
355        }),
356        category: ToolCategory::Memory,
357    }
358}
359
360fn knowledge_add_entity() -> ToolDefinition {
361    ToolDefinition {
362        name: "knowledge_add_entity".into(),
363        description: "Add an entity to your knowledge graph.".into(),
364        input_schema: serde_json::json!({
365            "type": "object",
366            "properties": {
367                "name": {
368                    "type": "string",
369                    "description": "Name of the entity."
370                },
371                "entity_type": {
372                    "type": "string",
373                    "description": "Type of entity (e.g. 'person', 'company', 'concept')."
374                },
375                "properties": {
376                    "type": "object",
377                    "description": "Additional properties as key-value pairs."
378                }
379            },
380            "required": ["name", "entity_type"]
381        }),
382        category: ToolCategory::Knowledge,
383    }
384}
385
386fn knowledge_add_relation() -> ToolDefinition {
387    ToolDefinition {
388        name: "knowledge_add_relation".into(),
389        description: "Add a relation between two entities in your knowledge graph.".into(),
390        input_schema: serde_json::json!({
391            "type": "object",
392            "properties": {
393                "from": {
394                    "type": "string",
395                    "description": "Source entity name."
396                },
397                "relation": {
398                    "type": "string",
399                    "description": "The relation type (e.g. 'works_at', 'depends_on')."
400                },
401                "to": {
402                    "type": "string",
403                    "description": "Target entity name."
404                },
405                "properties": {
406                    "type": "object",
407                    "description": "Additional properties."
408                }
409            },
410            "required": ["from", "relation", "to"]
411        }),
412        category: ToolCategory::Knowledge,
413    }
414}
415
416fn knowledge_query() -> ToolDefinition {
417    ToolDefinition {
418        name: "knowledge_query".into(),
419        description: "Search your knowledge graph for entities and their relations.".into(),
420        input_schema: serde_json::json!({
421            "type": "object",
422            "properties": {
423                "query": {
424                    "type": "string",
425                    "description": "Search query to find entities."
426                }
427            },
428            "required": ["query"]
429        }),
430        category: ToolCategory::Knowledge,
431    }
432}
433
434// ---------------------------------------------------------------------------
435// Agent coordination tools
436// ---------------------------------------------------------------------------
437
438fn agent_spawn() -> ToolDefinition {
439    ToolDefinition {
440        name: "agent_spawn".into(),
441        description: "Spawn a new fighter (AI agent). Returns the new fighter's ID. Use this to create subordinate agents that can handle specialized tasks.".into(),
442        input_schema: serde_json::json!({
443            "type": "object",
444            "properties": {
445                "name": {
446                    "type": "string",
447                    "description": "A human-readable name for the new fighter."
448                },
449                "system_prompt": {
450                    "type": "string",
451                    "description": "The system prompt that shapes the new fighter's behavior and specialization."
452                },
453                "description": {
454                    "type": "string",
455                    "description": "A short description of the fighter's purpose (optional)."
456                },
457                "capabilities": {
458                    "type": "array",
459                    "description": "Capabilities to grant the new fighter (optional). Each item is a capability object.",
460                    "items": {
461                        "type": "object"
462                    }
463                }
464            },
465            "required": ["name", "system_prompt"]
466        }),
467        category: ToolCategory::Agent,
468    }
469}
470
471fn agent_message() -> ToolDefinition {
472    ToolDefinition {
473        name: "agent_message".into(),
474        description: "Send a message to another fighter by ID or name and get its response. Use this for inter-agent coordination and delegation.".into(),
475        input_schema: serde_json::json!({
476            "type": "object",
477            "properties": {
478                "fighter_id": {
479                    "type": "string",
480                    "description": "The UUID of the target fighter (provide either this or 'name')."
481                },
482                "name": {
483                    "type": "string",
484                    "description": "The name of the target fighter (provide either this or 'fighter_id')."
485                },
486                "message": {
487                    "type": "string",
488                    "description": "The message to send to the target fighter."
489                }
490            },
491            "required": ["message"]
492        }),
493        category: ToolCategory::Agent,
494    }
495}
496
497fn agent_list() -> ToolDefinition {
498    ToolDefinition {
499        name: "agent_list".into(),
500        description: "List all active fighters (AI agents) with their IDs, names, and status."
501            .into(),
502        input_schema: serde_json::json!({
503            "type": "object",
504            "properties": {}
505        }),
506        category: ToolCategory::Agent,
507    }
508}
509
510// ---------------------------------------------------------------------------
511// Patch tools — combo move corrections
512// ---------------------------------------------------------------------------
513
514fn patch_apply() -> ToolDefinition {
515    ToolDefinition {
516        name: "patch_apply".into(),
517        description: "Apply a unified diff patch to a file. Reads the file, validates the patch, \
518                       applies it, and writes the result back. Supports standard unified diff format."
519            .into(),
520        input_schema: serde_json::json!({
521            "type": "object",
522            "properties": {
523                "path": {
524                    "type": "string",
525                    "description": "The file path to patch (relative to working directory or absolute)."
526                },
527                "diff": {
528                    "type": "string",
529                    "description": "The unified diff text to apply to the file."
530                }
531            },
532            "required": ["path", "diff"]
533        }),
534        category: ToolCategory::FileSystem,
535    }
536}
537
538// ---------------------------------------------------------------------------
539// Browser automation tools — ring-side scouting moves
540// ---------------------------------------------------------------------------
541
542fn browser_navigate() -> ToolDefinition {
543    ToolDefinition {
544        name: "browser_navigate".into(),
545        description: "Navigate the browser to a URL. Opens the page and waits for it to load."
546            .into(),
547        input_schema: serde_json::json!({
548            "type": "object",
549            "properties": {
550                "url": {
551                    "type": "string",
552                    "description": "The URL to navigate to."
553                }
554            },
555            "required": ["url"]
556        }),
557        category: ToolCategory::Browser,
558    }
559}
560
561fn browser_screenshot() -> ToolDefinition {
562    ToolDefinition {
563        name: "browser_screenshot".into(),
564        description: "Take a screenshot of the current page. Returns a base64-encoded PNG image."
565            .into(),
566        input_schema: serde_json::json!({
567            "type": "object",
568            "properties": {
569                "full_page": {
570                    "type": "boolean",
571                    "description": "Capture the full scrollable page (true) or just the viewport (false). Default: false."
572                }
573            }
574        }),
575        category: ToolCategory::Browser,
576    }
577}
578
579fn browser_click() -> ToolDefinition {
580    ToolDefinition {
581        name: "browser_click".into(),
582        description: "Click an element on the page matching the given CSS selector.".into(),
583        input_schema: serde_json::json!({
584            "type": "object",
585            "properties": {
586                "selector": {
587                    "type": "string",
588                    "description": "CSS selector of the element to click."
589                }
590            },
591            "required": ["selector"]
592        }),
593        category: ToolCategory::Browser,
594    }
595}
596
597fn browser_type() -> ToolDefinition {
598    ToolDefinition {
599        name: "browser_type".into(),
600        description: "Type text into an input element matching the given CSS selector.".into(),
601        input_schema: serde_json::json!({
602            "type": "object",
603            "properties": {
604                "selector": {
605                    "type": "string",
606                    "description": "CSS selector of the input element."
607                },
608                "text": {
609                    "type": "string",
610                    "description": "The text to type into the element."
611                }
612            },
613            "required": ["selector", "text"]
614        }),
615        category: ToolCategory::Browser,
616    }
617}
618
619fn browser_content() -> ToolDefinition {
620    ToolDefinition {
621        name: "browser_content".into(),
622        description:
623            "Get the text content of the page or a specific element. Useful for extracting readable text from a web page."
624                .into(),
625        input_schema: serde_json::json!({
626            "type": "object",
627            "properties": {
628                "selector": {
629                    "type": "string",
630                    "description": "Optional CSS selector. If omitted, returns the full page text content."
631                }
632            }
633        }),
634        category: ToolCategory::Browser,
635    }
636}
637
638// ---------------------------------------------------------------------------
639// Git / Source Control tools
640// ---------------------------------------------------------------------------
641
642fn git_status() -> ToolDefinition {
643    ToolDefinition {
644        name: "git_status".into(),
645        description: "Run `git status --porcelain` in the working directory to show changed files."
646            .into(),
647        input_schema: serde_json::json!({
648            "type": "object",
649            "properties": {}
650        }),
651        category: ToolCategory::SourceControl,
652    }
653}
654
655fn git_diff() -> ToolDefinition {
656    ToolDefinition {
657        name: "git_diff".into(),
658        description:
659            "Run `git diff` to show unstaged changes. Use `staged: true` to see staged changes."
660                .into(),
661        input_schema: serde_json::json!({
662            "type": "object",
663            "properties": {
664                "staged": {
665                    "type": "boolean",
666                    "description": "If true, show staged changes (--staged). Default: false."
667                },
668                "path": {
669                    "type": "string",
670                    "description": "Optional file path to restrict the diff to."
671                }
672            }
673        }),
674        category: ToolCategory::SourceControl,
675    }
676}
677
678fn git_log() -> ToolDefinition {
679    ToolDefinition {
680        name: "git_log".into(),
681        description: "Show recent git commits with `git log --oneline`.".into(),
682        input_schema: serde_json::json!({
683            "type": "object",
684            "properties": {
685                "count": {
686                    "type": "integer",
687                    "description": "Number of commits to show (default: 10)."
688                }
689            }
690        }),
691        category: ToolCategory::SourceControl,
692    }
693}
694
695fn git_commit() -> ToolDefinition {
696    ToolDefinition {
697        name: "git_commit".into(),
698        description: "Stage files and create a git commit with the given message.".into(),
699        input_schema: serde_json::json!({
700            "type": "object",
701            "properties": {
702                "message": {
703                    "type": "string",
704                    "description": "The commit message."
705                },
706                "files": {
707                    "type": "array",
708                    "items": { "type": "string" },
709                    "description": "Files to stage before committing. If empty, commits all staged changes."
710                }
711            },
712            "required": ["message"]
713        }),
714        category: ToolCategory::SourceControl,
715    }
716}
717
718fn git_branch() -> ToolDefinition {
719    ToolDefinition {
720        name: "git_branch".into(),
721        description: "List, create, or switch git branches.".into(),
722        input_schema: serde_json::json!({
723            "type": "object",
724            "properties": {
725                "action": {
726                    "type": "string",
727                    "enum": ["list", "create", "switch"],
728                    "description": "Action to perform: list, create, or switch. Default: list."
729                },
730                "name": {
731                    "type": "string",
732                    "description": "Branch name (required for create and switch)."
733                }
734            }
735        }),
736        category: ToolCategory::SourceControl,
737    }
738}
739
740// ---------------------------------------------------------------------------
741// Container tools
742// ---------------------------------------------------------------------------
743
744fn docker_ps() -> ToolDefinition {
745    ToolDefinition {
746        name: "docker_ps".into(),
747        description: "List running Docker containers.".into(),
748        input_schema: serde_json::json!({
749            "type": "object",
750            "properties": {
751                "all": {
752                    "type": "boolean",
753                    "description": "Show all containers, not just running ones. Default: false."
754                }
755            }
756        }),
757        category: ToolCategory::Container,
758    }
759}
760
761fn docker_run() -> ToolDefinition {
762    ToolDefinition {
763        name: "docker_run".into(),
764        description: "Run a Docker container from an image.".into(),
765        input_schema: serde_json::json!({
766            "type": "object",
767            "properties": {
768                "image": {
769                    "type": "string",
770                    "description": "The Docker image to run."
771                },
772                "command": {
773                    "type": "string",
774                    "description": "Optional command to run inside the container."
775                },
776                "env": {
777                    "type": "object",
778                    "description": "Environment variables as key-value pairs."
779                },
780                "ports": {
781                    "type": "array",
782                    "items": { "type": "string" },
783                    "description": "Port mappings (e.g. '8080:80')."
784                },
785                "detach": {
786                    "type": "boolean",
787                    "description": "Run in detached mode. Default: false."
788                },
789                "name": {
790                    "type": "string",
791                    "description": "Optional container name."
792                }
793            },
794            "required": ["image"]
795        }),
796        category: ToolCategory::Container,
797    }
798}
799
800fn docker_build() -> ToolDefinition {
801    ToolDefinition {
802        name: "docker_build".into(),
803        description: "Build a Docker image from a Dockerfile.".into(),
804        input_schema: serde_json::json!({
805            "type": "object",
806            "properties": {
807                "path": {
808                    "type": "string",
809                    "description": "Path to the build context directory (default: '.')."
810                },
811                "tag": {
812                    "type": "string",
813                    "description": "Tag for the built image (e.g. 'myapp:latest')."
814                },
815                "dockerfile": {
816                    "type": "string",
817                    "description": "Path to the Dockerfile (default: 'Dockerfile')."
818                }
819            }
820        }),
821        category: ToolCategory::Container,
822    }
823}
824
825fn docker_logs() -> ToolDefinition {
826    ToolDefinition {
827        name: "docker_logs".into(),
828        description: "Get logs from a Docker container.".into(),
829        input_schema: serde_json::json!({
830            "type": "object",
831            "properties": {
832                "container": {
833                    "type": "string",
834                    "description": "Container ID or name."
835                },
836                "tail": {
837                    "type": "integer",
838                    "description": "Number of lines to show from the end (default: 100)."
839                }
840            },
841            "required": ["container"]
842        }),
843        category: ToolCategory::Container,
844    }
845}
846
847// ---------------------------------------------------------------------------
848// HTTP tools
849// ---------------------------------------------------------------------------
850
851fn http_request() -> ToolDefinition {
852    ToolDefinition {
853        name: "http_request".into(),
854        description: "Send a full HTTP request with custom method, headers, body, and timeout."
855            .into(),
856        input_schema: serde_json::json!({
857            "type": "object",
858            "properties": {
859                "url": {
860                    "type": "string",
861                    "description": "The URL to send the request to."
862                },
863                "method": {
864                    "type": "string",
865                    "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"],
866                    "description": "HTTP method. Default: GET."
867                },
868                "headers": {
869                    "type": "object",
870                    "description": "Request headers as key-value pairs."
871                },
872                "body": {
873                    "type": "string",
874                    "description": "Request body."
875                },
876                "timeout_secs": {
877                    "type": "integer",
878                    "description": "Request timeout in seconds (default: 30)."
879                }
880            },
881            "required": ["url"]
882        }),
883        category: ToolCategory::Web,
884    }
885}
886
887fn http_post() -> ToolDefinition {
888    ToolDefinition {
889        name: "http_post".into(),
890        description: "Shorthand for an HTTP POST request with a JSON body.".into(),
891        input_schema: serde_json::json!({
892            "type": "object",
893            "properties": {
894                "url": {
895                    "type": "string",
896                    "description": "The URL to POST to."
897                },
898                "json": {
899                    "type": "object",
900                    "description": "JSON body to send."
901                },
902                "headers": {
903                    "type": "object",
904                    "description": "Additional headers."
905                }
906            },
907            "required": ["url", "json"]
908        }),
909        category: ToolCategory::Web,
910    }
911}
912
913// ---------------------------------------------------------------------------
914// Data manipulation tools
915// ---------------------------------------------------------------------------
916
917fn json_query() -> ToolDefinition {
918    ToolDefinition {
919        name: "json_query".into(),
920        description: "Query a JSON value using a dot-separated path (e.g. 'users.0.name'). Array indices are numeric.".into(),
921        input_schema: serde_json::json!({
922            "type": "object",
923            "properties": {
924                "data": {
925                    "description": "The JSON data to query (object, array, or string to parse)."
926                },
927                "path": {
928                    "type": "string",
929                    "description": "Dot-separated path to query (e.g. 'a.b.0.c')."
930                }
931            },
932            "required": ["data", "path"]
933        }),
934        category: ToolCategory::Data,
935    }
936}
937
938fn json_transform() -> ToolDefinition {
939    ToolDefinition {
940        name: "json_transform".into(),
941        description: "Transform JSON data: extract specific keys, rename keys, or filter an array of objects.".into(),
942        input_schema: serde_json::json!({
943            "type": "object",
944            "properties": {
945                "data": {
946                    "description": "The JSON data to transform."
947                },
948                "extract": {
949                    "type": "array",
950                    "items": { "type": "string" },
951                    "description": "List of keys to extract from each object."
952                },
953                "rename": {
954                    "type": "object",
955                    "description": "Key rename mapping (old_name -> new_name)."
956                },
957                "filter_key": {
958                    "type": "string",
959                    "description": "Key to filter array items by."
960                },
961                "filter_value": {
962                    "type": "string",
963                    "description": "Value the filter_key must match."
964                }
965            },
966            "required": ["data"]
967        }),
968        category: ToolCategory::Data,
969    }
970}
971
972fn yaml_parse() -> ToolDefinition {
973    ToolDefinition {
974        name: "yaml_parse".into(),
975        description: "Parse a YAML string and return it as JSON.".into(),
976        input_schema: serde_json::json!({
977            "type": "object",
978            "properties": {
979                "content": {
980                    "type": "string",
981                    "description": "The YAML string to parse."
982                }
983            },
984            "required": ["content"]
985        }),
986        category: ToolCategory::Data,
987    }
988}
989
990fn regex_match() -> ToolDefinition {
991    ToolDefinition {
992        name: "regex_match".into(),
993        description: "Match a regex pattern against text and return all captures.".into(),
994        input_schema: serde_json::json!({
995            "type": "object",
996            "properties": {
997                "pattern": {
998                    "type": "string",
999                    "description": "The regex pattern."
1000                },
1001                "text": {
1002                    "type": "string",
1003                    "description": "The text to match against."
1004                },
1005                "global": {
1006                    "type": "boolean",
1007                    "description": "Find all matches (true) or just the first (false). Default: false."
1008                }
1009            },
1010            "required": ["pattern", "text"]
1011        }),
1012        category: ToolCategory::Data,
1013    }
1014}
1015
1016fn regex_replace() -> ToolDefinition {
1017    ToolDefinition {
1018        name: "regex_replace".into(),
1019        description: "Find and replace text using a regex pattern. Supports capture group references ($1, $2, etc.) in the replacement.".into(),
1020        input_schema: serde_json::json!({
1021            "type": "object",
1022            "properties": {
1023                "pattern": {
1024                    "type": "string",
1025                    "description": "The regex pattern to find."
1026                },
1027                "replacement": {
1028                    "type": "string",
1029                    "description": "The replacement string (supports $1, $2, etc. for captures)."
1030                },
1031                "text": {
1032                    "type": "string",
1033                    "description": "The text to perform replacement on."
1034                }
1035            },
1036            "required": ["pattern", "replacement", "text"]
1037        }),
1038        category: ToolCategory::Data,
1039    }
1040}
1041
1042// ---------------------------------------------------------------------------
1043// Process tools
1044// ---------------------------------------------------------------------------
1045
1046fn process_list() -> ToolDefinition {
1047    ToolDefinition {
1048        name: "process_list".into(),
1049        description: "List running processes with PID, name, and CPU/memory usage.".into(),
1050        input_schema: serde_json::json!({
1051            "type": "object",
1052            "properties": {
1053                "filter": {
1054                    "type": "string",
1055                    "description": "Optional filter string to match process names."
1056                }
1057            }
1058        }),
1059        category: ToolCategory::Shell,
1060    }
1061}
1062
1063fn process_kill() -> ToolDefinition {
1064    ToolDefinition {
1065        name: "process_kill".into(),
1066        description: "Kill a process by PID.".into(),
1067        input_schema: serde_json::json!({
1068            "type": "object",
1069            "properties": {
1070                "pid": {
1071                    "type": "integer",
1072                    "description": "The process ID to kill."
1073                },
1074                "signal": {
1075                    "type": "string",
1076                    "description": "Signal to send (e.g. 'TERM', 'KILL'). Default: 'TERM'."
1077                }
1078            },
1079            "required": ["pid"]
1080        }),
1081        category: ToolCategory::Shell,
1082    }
1083}
1084
1085// ---------------------------------------------------------------------------
1086// Schedule tools
1087// ---------------------------------------------------------------------------
1088
1089fn schedule_task() -> ToolDefinition {
1090    ToolDefinition {
1091        name: "schedule_task".into(),
1092        description: "Schedule a one-shot or recurring task. Returns a task ID.".into(),
1093        input_schema: serde_json::json!({
1094            "type": "object",
1095            "properties": {
1096                "name": {
1097                    "type": "string",
1098                    "description": "Human-readable name for the task."
1099                },
1100                "command": {
1101                    "type": "string",
1102                    "description": "Shell command to execute when the task fires."
1103                },
1104                "delay_secs": {
1105                    "type": "integer",
1106                    "description": "Delay in seconds before first execution."
1107                },
1108                "interval_secs": {
1109                    "type": "integer",
1110                    "description": "Interval in seconds for recurring execution. If omitted, the task runs once."
1111                }
1112            },
1113            "required": ["name", "command", "delay_secs"]
1114        }),
1115        category: ToolCategory::Schedule,
1116    }
1117}
1118
1119fn schedule_list() -> ToolDefinition {
1120    ToolDefinition {
1121        name: "schedule_list".into(),
1122        description: "List all scheduled tasks with their IDs, names, and status.".into(),
1123        input_schema: serde_json::json!({
1124            "type": "object",
1125            "properties": {}
1126        }),
1127        category: ToolCategory::Schedule,
1128    }
1129}
1130
1131fn schedule_cancel() -> ToolDefinition {
1132    ToolDefinition {
1133        name: "schedule_cancel".into(),
1134        description: "Cancel a scheduled task by its ID.".into(),
1135        input_schema: serde_json::json!({
1136            "type": "object",
1137            "properties": {
1138                "task_id": {
1139                    "type": "string",
1140                    "description": "The UUID of the task to cancel."
1141                }
1142            },
1143            "required": ["task_id"]
1144        }),
1145        category: ToolCategory::Schedule,
1146    }
1147}
1148
1149// ---------------------------------------------------------------------------
1150// Code analysis tools
1151// ---------------------------------------------------------------------------
1152
1153fn code_search() -> ToolDefinition {
1154    ToolDefinition {
1155        name: "code_search".into(),
1156        description: "Search for text or a regex pattern in files recursively under a directory. Returns matching lines with file paths and line numbers.".into(),
1157        input_schema: serde_json::json!({
1158            "type": "object",
1159            "properties": {
1160                "pattern": {
1161                    "type": "string",
1162                    "description": "The regex pattern to search for."
1163                },
1164                "path": {
1165                    "type": "string",
1166                    "description": "Root directory to search in (default: working directory)."
1167                },
1168                "file_pattern": {
1169                    "type": "string",
1170                    "description": "Glob pattern to filter files (e.g. '*.rs', '*.py')."
1171                },
1172                "max_results": {
1173                    "type": "integer",
1174                    "description": "Maximum number of matches to return (default: 50)."
1175                }
1176            },
1177            "required": ["pattern"]
1178        }),
1179        category: ToolCategory::CodeAnalysis,
1180    }
1181}
1182
1183fn code_symbols() -> ToolDefinition {
1184    ToolDefinition {
1185        name: "code_symbols".into(),
1186        description: "Extract function, struct, class, and method definitions from a source file using regex-based heuristics.".into(),
1187        input_schema: serde_json::json!({
1188            "type": "object",
1189            "properties": {
1190                "path": {
1191                    "type": "string",
1192                    "description": "Path to the source file to analyze."
1193                }
1194            },
1195            "required": ["path"]
1196        }),
1197        category: ToolCategory::CodeAnalysis,
1198    }
1199}
1200
1201// ---------------------------------------------------------------------------
1202// Archive tools
1203// ---------------------------------------------------------------------------
1204
1205fn archive_create() -> ToolDefinition {
1206    ToolDefinition {
1207        name: "archive_create".into(),
1208        description: "Create a tar.gz archive from a list of file or directory paths.".into(),
1209        input_schema: serde_json::json!({
1210            "type": "object",
1211            "properties": {
1212                "output_path": {
1213                    "type": "string",
1214                    "description": "Path for the output .tar.gz archive file."
1215                },
1216                "paths": {
1217                    "type": "array",
1218                    "items": { "type": "string" },
1219                    "description": "List of file or directory paths to include in the archive."
1220                }
1221            },
1222            "required": ["output_path", "paths"]
1223        }),
1224        category: ToolCategory::Archive,
1225    }
1226}
1227
1228fn archive_extract() -> ToolDefinition {
1229    ToolDefinition {
1230        name: "archive_extract".into(),
1231        description: "Extract a tar.gz archive to a destination directory.".into(),
1232        input_schema: serde_json::json!({
1233            "type": "object",
1234            "properties": {
1235                "archive_path": {
1236                    "type": "string",
1237                    "description": "Path to the .tar.gz archive to extract."
1238                },
1239                "destination": {
1240                    "type": "string",
1241                    "description": "Directory to extract the archive into."
1242                }
1243            },
1244            "required": ["archive_path", "destination"]
1245        }),
1246        category: ToolCategory::Archive,
1247    }
1248}
1249
1250fn archive_list() -> ToolDefinition {
1251    ToolDefinition {
1252        name: "archive_list".into(),
1253        description: "List the contents of a tar.gz archive without extracting.".into(),
1254        input_schema: serde_json::json!({
1255            "type": "object",
1256            "properties": {
1257                "archive_path": {
1258                    "type": "string",
1259                    "description": "Path to the .tar.gz archive to list."
1260                }
1261            },
1262            "required": ["archive_path"]
1263        }),
1264        category: ToolCategory::Archive,
1265    }
1266}
1267
1268// ---------------------------------------------------------------------------
1269// Template tools
1270// ---------------------------------------------------------------------------
1271
1272fn template_render() -> ToolDefinition {
1273    ToolDefinition {
1274        name: "template_render".into(),
1275        description: "Render a Handlebars-style template by substituting {{variable}} placeholders with provided values.".into(),
1276        input_schema: serde_json::json!({
1277            "type": "object",
1278            "properties": {
1279                "template": {
1280                    "type": "string",
1281                    "description": "The template string containing {{variable}} placeholders."
1282                },
1283                "variables": {
1284                    "type": "object",
1285                    "description": "Key-value pairs mapping variable names to their values."
1286                }
1287            },
1288            "required": ["template", "variables"]
1289        }),
1290        category: ToolCategory::Template,
1291    }
1292}
1293
1294// ---------------------------------------------------------------------------
1295// Crypto / Hash tools
1296// ---------------------------------------------------------------------------
1297
1298fn hash_compute() -> ToolDefinition {
1299    ToolDefinition {
1300        name: "hash_compute".into(),
1301        description: "Compute a cryptographic hash (SHA-256, SHA-512, or MD5) of a string or file."
1302            .into(),
1303        input_schema: serde_json::json!({
1304            "type": "object",
1305            "properties": {
1306                "algorithm": {
1307                    "type": "string",
1308                    "enum": ["sha256", "sha512", "md5"],
1309                    "description": "Hash algorithm to use. Default: sha256."
1310                },
1311                "input": {
1312                    "type": "string",
1313                    "description": "The string to hash (provide either this or 'file')."
1314                },
1315                "file": {
1316                    "type": "string",
1317                    "description": "Path to a file to hash (provide either this or 'input')."
1318                }
1319            }
1320        }),
1321        category: ToolCategory::Crypto,
1322    }
1323}
1324
1325fn hash_verify() -> ToolDefinition {
1326    ToolDefinition {
1327        name: "hash_verify".into(),
1328        description: "Verify that a hash matches an expected value.".into(),
1329        input_schema: serde_json::json!({
1330            "type": "object",
1331            "properties": {
1332                "algorithm": {
1333                    "type": "string",
1334                    "enum": ["sha256", "sha512", "md5"],
1335                    "description": "Hash algorithm to use. Default: sha256."
1336                },
1337                "input": {
1338                    "type": "string",
1339                    "description": "The string to hash (provide either this or 'file')."
1340                },
1341                "file": {
1342                    "type": "string",
1343                    "description": "Path to a file to hash (provide either this or 'input')."
1344                },
1345                "expected": {
1346                    "type": "string",
1347                    "description": "The expected hex-encoded hash value to compare against."
1348                }
1349            },
1350            "required": ["expected"]
1351        }),
1352        category: ToolCategory::Crypto,
1353    }
1354}
1355
1356// ---------------------------------------------------------------------------
1357// Environment tools
1358// ---------------------------------------------------------------------------
1359
1360fn env_get() -> ToolDefinition {
1361    ToolDefinition {
1362        name: "env_get".into(),
1363        description: "Get the value of an environment variable.".into(),
1364        input_schema: serde_json::json!({
1365            "type": "object",
1366            "properties": {
1367                "name": {
1368                    "type": "string",
1369                    "description": "The environment variable name."
1370                }
1371            },
1372            "required": ["name"]
1373        }),
1374        category: ToolCategory::Shell,
1375    }
1376}
1377
1378fn env_list() -> ToolDefinition {
1379    ToolDefinition {
1380        name: "env_list".into(),
1381        description: "List all environment variables, with optional prefix filter.".into(),
1382        input_schema: serde_json::json!({
1383            "type": "object",
1384            "properties": {
1385                "prefix": {
1386                    "type": "string",
1387                    "description": "Optional prefix to filter environment variable names by."
1388                }
1389            }
1390        }),
1391        category: ToolCategory::Shell,
1392    }
1393}
1394
1395// ---------------------------------------------------------------------------
1396// Text tools
1397// ---------------------------------------------------------------------------
1398
1399fn text_diff() -> ToolDefinition {
1400    ToolDefinition {
1401        name: "text_diff".into(),
1402        description: "Compute a unified diff between two text strings.".into(),
1403        input_schema: serde_json::json!({
1404            "type": "object",
1405            "properties": {
1406                "old_text": {
1407                    "type": "string",
1408                    "description": "The original text."
1409                },
1410                "new_text": {
1411                    "type": "string",
1412                    "description": "The modified text."
1413                },
1414                "label": {
1415                    "type": "string",
1416                    "description": "Optional label for the diff output (default: 'a' / 'b')."
1417                }
1418            },
1419            "required": ["old_text", "new_text"]
1420        }),
1421        category: ToolCategory::Data,
1422    }
1423}
1424
1425fn text_count() -> ToolDefinition {
1426    ToolDefinition {
1427        name: "text_count".into(),
1428        description: "Count lines, words, and characters in text.".into(),
1429        input_schema: serde_json::json!({
1430            "type": "object",
1431            "properties": {
1432                "text": {
1433                    "type": "string",
1434                    "description": "The text to count."
1435                }
1436            },
1437            "required": ["text"]
1438        }),
1439        category: ToolCategory::Data,
1440    }
1441}
1442
1443// ---------------------------------------------------------------------------
1444// File tools (extended)
1445// ---------------------------------------------------------------------------
1446
1447fn file_search() -> ToolDefinition {
1448    ToolDefinition {
1449        name: "file_search".into(),
1450        description: "Search for files by name pattern (glob) recursively under a directory."
1451            .into(),
1452        input_schema: serde_json::json!({
1453            "type": "object",
1454            "properties": {
1455                "pattern": {
1456                    "type": "string",
1457                    "description": "Glob pattern to match file names (e.g. '*.rs', 'Cargo.*')."
1458                },
1459                "path": {
1460                    "type": "string",
1461                    "description": "Root directory to search in (default: working directory)."
1462                },
1463                "max_results": {
1464                    "type": "integer",
1465                    "description": "Maximum number of results to return (default: 100)."
1466                }
1467            },
1468            "required": ["pattern"]
1469        }),
1470        category: ToolCategory::FileSystem,
1471    }
1472}
1473
1474fn file_info() -> ToolDefinition {
1475    ToolDefinition {
1476        name: "file_info".into(),
1477        description: "Get file metadata: size, modified time, permissions, and type.".into(),
1478        input_schema: serde_json::json!({
1479            "type": "object",
1480            "properties": {
1481                "path": {
1482                    "type": "string",
1483                    "description": "Path to the file or directory to inspect."
1484                }
1485            },
1486            "required": ["path"]
1487        }),
1488        category: ToolCategory::FileSystem,
1489    }
1490}
1491
1492// ---------------------------------------------------------------------------
1493// Tests
1494// ---------------------------------------------------------------------------
1495
1496#[cfg(test)]
1497mod tests {
1498    use super::*;
1499
1500    #[test]
1501    fn test_browser_tool_definitions_correct() {
1502        let nav = browser_navigate();
1503        assert_eq!(nav.name, "browser_navigate");
1504        assert_eq!(nav.category, ToolCategory::Browser);
1505        assert!(nav.input_schema["required"]
1506            .as_array()
1507            .expect("required should be array")
1508            .iter()
1509            .any(|v| v == "url"));
1510
1511        let ss = browser_screenshot();
1512        assert_eq!(ss.name, "browser_screenshot");
1513        assert_eq!(ss.category, ToolCategory::Browser);
1514
1515        let click = browser_click();
1516        assert_eq!(click.name, "browser_click");
1517        assert!(click.input_schema["required"]
1518            .as_array()
1519            .expect("required should be array")
1520            .iter()
1521            .any(|v| v == "selector"));
1522
1523        let typ = browser_type();
1524        assert_eq!(typ.name, "browser_type");
1525        let required = typ.input_schema["required"]
1526            .as_array()
1527            .expect("required should be array");
1528        assert!(required.iter().any(|v| v == "selector"));
1529        assert!(required.iter().any(|v| v == "text"));
1530
1531        let content = browser_content();
1532        assert_eq!(content.name, "browser_content");
1533        assert_eq!(content.category, ToolCategory::Browser);
1534    }
1535
1536    #[test]
1537    fn test_browser_tools_require_browser_control_capability() {
1538        let caps = vec![Capability::BrowserControl];
1539        let tools = tools_for_capabilities(&caps);
1540
1541        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
1542        assert!(
1543            names.contains(&"browser_navigate"),
1544            "missing browser_navigate"
1545        );
1546        assert!(
1547            names.contains(&"browser_screenshot"),
1548            "missing browser_screenshot"
1549        );
1550        assert!(names.contains(&"browser_click"), "missing browser_click");
1551        assert!(names.contains(&"browser_type"), "missing browser_type");
1552        assert!(
1553            names.contains(&"browser_content"),
1554            "missing browser_content"
1555        );
1556    }
1557
1558    #[test]
1559    fn test_browser_tools_absent_without_capability() {
1560        let caps = vec![Capability::Memory];
1561        let tools = tools_for_capabilities(&caps);
1562
1563        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
1564        assert!(!names.iter().any(|n| n.starts_with("browser_")));
1565    }
1566
1567    #[test]
1568    fn test_all_tools_includes_browser() {
1569        let tools = all_tools();
1570        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
1571        assert!(names.contains(&"browser_navigate"));
1572        assert!(names.contains(&"browser_screenshot"));
1573        assert!(names.contains(&"browser_click"));
1574        assert!(names.contains(&"browser_type"));
1575        assert!(names.contains(&"browser_content"));
1576    }
1577
1578    // -----------------------------------------------------------------------
1579    // Tool definition correctness tests
1580    // -----------------------------------------------------------------------
1581
1582    #[test]
1583    fn test_file_read_definition() {
1584        let t = file_read();
1585        assert_eq!(t.name, "file_read");
1586        assert_eq!(t.category, ToolCategory::FileSystem);
1587        assert_eq!(t.input_schema["type"], "object");
1588        let required = t.input_schema["required"].as_array().unwrap();
1589        assert!(required.iter().any(|v| v == "path"));
1590    }
1591
1592    #[test]
1593    fn test_file_write_definition() {
1594        let t = file_write();
1595        assert_eq!(t.name, "file_write");
1596        assert_eq!(t.category, ToolCategory::FileSystem);
1597        let required = t.input_schema["required"].as_array().unwrap();
1598        assert!(required.iter().any(|v| v == "path"));
1599        assert!(required.iter().any(|v| v == "content"));
1600    }
1601
1602    #[test]
1603    fn test_file_list_definition() {
1604        let t = file_list();
1605        assert_eq!(t.name, "file_list");
1606        assert_eq!(t.category, ToolCategory::FileSystem);
1607        assert_eq!(t.input_schema["type"], "object");
1608    }
1609
1610    #[test]
1611    fn test_file_search_definition() {
1612        let t = file_search();
1613        assert_eq!(t.name, "file_search");
1614        assert_eq!(t.category, ToolCategory::FileSystem);
1615        let required = t.input_schema["required"].as_array().unwrap();
1616        assert!(required.iter().any(|v| v == "pattern"));
1617    }
1618
1619    #[test]
1620    fn test_file_info_definition() {
1621        let t = file_info();
1622        assert_eq!(t.name, "file_info");
1623        assert_eq!(t.category, ToolCategory::FileSystem);
1624        let required = t.input_schema["required"].as_array().unwrap();
1625        assert!(required.iter().any(|v| v == "path"));
1626    }
1627
1628    #[test]
1629    fn test_patch_apply_definition() {
1630        let t = patch_apply();
1631        assert_eq!(t.name, "patch_apply");
1632        assert_eq!(t.category, ToolCategory::FileSystem);
1633        let required = t.input_schema["required"].as_array().unwrap();
1634        assert!(required.iter().any(|v| v == "path"));
1635        assert!(required.iter().any(|v| v == "diff"));
1636    }
1637
1638    #[test]
1639    fn test_shell_exec_definition() {
1640        let t = shell_exec();
1641        assert_eq!(t.name, "shell_exec");
1642        assert_eq!(t.category, ToolCategory::Shell);
1643        let required = t.input_schema["required"].as_array().unwrap();
1644        assert!(required.iter().any(|v| v == "command"));
1645    }
1646
1647    #[test]
1648    fn test_web_fetch_definition() {
1649        let t = web_fetch();
1650        assert_eq!(t.name, "web_fetch");
1651        assert_eq!(t.category, ToolCategory::Web);
1652        let required = t.input_schema["required"].as_array().unwrap();
1653        assert!(required.iter().any(|v| v == "url"));
1654    }
1655
1656    #[test]
1657    fn test_web_search_definition() {
1658        let t = web_search();
1659        assert_eq!(t.name, "web_search");
1660        assert_eq!(t.category, ToolCategory::Web);
1661        let required = t.input_schema["required"].as_array().unwrap();
1662        assert!(required.iter().any(|v| v == "query"));
1663    }
1664
1665    #[test]
1666    fn test_memory_store_definition() {
1667        let t = memory_store();
1668        assert_eq!(t.name, "memory_store");
1669        assert_eq!(t.category, ToolCategory::Memory);
1670        let required = t.input_schema["required"].as_array().unwrap();
1671        assert!(required.iter().any(|v| v == "key"));
1672        assert!(required.iter().any(|v| v == "value"));
1673    }
1674
1675    #[test]
1676    fn test_memory_recall_definition() {
1677        let t = memory_recall();
1678        assert_eq!(t.name, "memory_recall");
1679        assert_eq!(t.category, ToolCategory::Memory);
1680        let required = t.input_schema["required"].as_array().unwrap();
1681        assert!(required.iter().any(|v| v == "query"));
1682    }
1683
1684    #[test]
1685    fn test_knowledge_tools_definitions() {
1686        let ae = knowledge_add_entity();
1687        assert_eq!(ae.name, "knowledge_add_entity");
1688        assert_eq!(ae.category, ToolCategory::Knowledge);
1689        let required = ae.input_schema["required"].as_array().unwrap();
1690        assert!(required.iter().any(|v| v == "name"));
1691        assert!(required.iter().any(|v| v == "entity_type"));
1692
1693        let ar = knowledge_add_relation();
1694        assert_eq!(ar.name, "knowledge_add_relation");
1695        let required = ar.input_schema["required"].as_array().unwrap();
1696        assert!(required.iter().any(|v| v == "from"));
1697        assert!(required.iter().any(|v| v == "relation"));
1698        assert!(required.iter().any(|v| v == "to"));
1699
1700        let kq = knowledge_query();
1701        assert_eq!(kq.name, "knowledge_query");
1702        let required = kq.input_schema["required"].as_array().unwrap();
1703        assert!(required.iter().any(|v| v == "query"));
1704    }
1705
1706    #[test]
1707    fn test_agent_tools_definitions() {
1708        let spawn = agent_spawn();
1709        assert_eq!(spawn.name, "agent_spawn");
1710        assert_eq!(spawn.category, ToolCategory::Agent);
1711        let required = spawn.input_schema["required"].as_array().unwrap();
1712        assert!(required.iter().any(|v| v == "name"));
1713        assert!(required.iter().any(|v| v == "system_prompt"));
1714
1715        let msg = agent_message();
1716        assert_eq!(msg.name, "agent_message");
1717        assert_eq!(msg.category, ToolCategory::Agent);
1718        let required = msg.input_schema["required"].as_array().unwrap();
1719        assert!(required.iter().any(|v| v == "message"));
1720
1721        let list = agent_list();
1722        assert_eq!(list.name, "agent_list");
1723        assert_eq!(list.category, ToolCategory::Agent);
1724    }
1725
1726    #[test]
1727    fn test_git_tools_definitions() {
1728        let status = git_status();
1729        assert_eq!(status.name, "git_status");
1730        assert_eq!(status.category, ToolCategory::SourceControl);
1731
1732        let diff = git_diff();
1733        assert_eq!(diff.name, "git_diff");
1734
1735        let log = git_log();
1736        assert_eq!(log.name, "git_log");
1737
1738        let commit = git_commit();
1739        assert_eq!(commit.name, "git_commit");
1740        let required = commit.input_schema["required"].as_array().unwrap();
1741        assert!(required.iter().any(|v| v == "message"));
1742
1743        let branch = git_branch();
1744        assert_eq!(branch.name, "git_branch");
1745    }
1746
1747    #[test]
1748    fn test_docker_tools_definitions() {
1749        let ps = docker_ps();
1750        assert_eq!(ps.name, "docker_ps");
1751        assert_eq!(ps.category, ToolCategory::Container);
1752
1753        let run = docker_run();
1754        assert_eq!(run.name, "docker_run");
1755        let required = run.input_schema["required"].as_array().unwrap();
1756        assert!(required.iter().any(|v| v == "image"));
1757
1758        let build = docker_build();
1759        assert_eq!(build.name, "docker_build");
1760
1761        let logs = docker_logs();
1762        assert_eq!(logs.name, "docker_logs");
1763        let required = logs.input_schema["required"].as_array().unwrap();
1764        assert!(required.iter().any(|v| v == "container"));
1765    }
1766
1767    #[test]
1768    fn test_http_tools_definitions() {
1769        let req = http_request();
1770        assert_eq!(req.name, "http_request");
1771        assert_eq!(req.category, ToolCategory::Web);
1772        let required = req.input_schema["required"].as_array().unwrap();
1773        assert!(required.iter().any(|v| v == "url"));
1774
1775        let post = http_post();
1776        assert_eq!(post.name, "http_post");
1777        let required = post.input_schema["required"].as_array().unwrap();
1778        assert!(required.iter().any(|v| v == "url"));
1779        assert!(required.iter().any(|v| v == "json"));
1780    }
1781
1782    #[test]
1783    fn test_data_tools_definitions() {
1784        let jq = json_query();
1785        assert_eq!(jq.name, "json_query");
1786        assert_eq!(jq.category, ToolCategory::Data);
1787        let required = jq.input_schema["required"].as_array().unwrap();
1788        assert!(required.iter().any(|v| v == "data"));
1789        assert!(required.iter().any(|v| v == "path"));
1790
1791        let jt = json_transform();
1792        assert_eq!(jt.name, "json_transform");
1793        let required = jt.input_schema["required"].as_array().unwrap();
1794        assert!(required.iter().any(|v| v == "data"));
1795
1796        let yp = yaml_parse();
1797        assert_eq!(yp.name, "yaml_parse");
1798        let required = yp.input_schema["required"].as_array().unwrap();
1799        assert!(required.iter().any(|v| v == "content"));
1800
1801        let rm = regex_match();
1802        assert_eq!(rm.name, "regex_match");
1803        let required = rm.input_schema["required"].as_array().unwrap();
1804        assert!(required.iter().any(|v| v == "pattern"));
1805        assert!(required.iter().any(|v| v == "text"));
1806
1807        let rr = regex_replace();
1808        assert_eq!(rr.name, "regex_replace");
1809        let required = rr.input_schema["required"].as_array().unwrap();
1810        assert!(required.iter().any(|v| v == "pattern"));
1811        assert!(required.iter().any(|v| v == "replacement"));
1812        assert!(required.iter().any(|v| v == "text"));
1813    }
1814
1815    #[test]
1816    fn test_process_tools_definitions() {
1817        let pl = process_list();
1818        assert_eq!(pl.name, "process_list");
1819        assert_eq!(pl.category, ToolCategory::Shell);
1820
1821        let pk = process_kill();
1822        assert_eq!(pk.name, "process_kill");
1823        let required = pk.input_schema["required"].as_array().unwrap();
1824        assert!(required.iter().any(|v| v == "pid"));
1825    }
1826
1827    #[test]
1828    fn test_schedule_tools_definitions() {
1829        let st = schedule_task();
1830        assert_eq!(st.name, "schedule_task");
1831        assert_eq!(st.category, ToolCategory::Schedule);
1832        let required = st.input_schema["required"].as_array().unwrap();
1833        assert!(required.iter().any(|v| v == "name"));
1834        assert!(required.iter().any(|v| v == "command"));
1835        assert!(required.iter().any(|v| v == "delay_secs"));
1836
1837        let sl = schedule_list();
1838        assert_eq!(sl.name, "schedule_list");
1839
1840        let sc = schedule_cancel();
1841        assert_eq!(sc.name, "schedule_cancel");
1842        let required = sc.input_schema["required"].as_array().unwrap();
1843        assert!(required.iter().any(|v| v == "task_id"));
1844    }
1845
1846    #[test]
1847    fn test_code_analysis_tools_definitions() {
1848        let cs = code_search();
1849        assert_eq!(cs.name, "code_search");
1850        assert_eq!(cs.category, ToolCategory::CodeAnalysis);
1851        let required = cs.input_schema["required"].as_array().unwrap();
1852        assert!(required.iter().any(|v| v == "pattern"));
1853
1854        let sym = code_symbols();
1855        assert_eq!(sym.name, "code_symbols");
1856        let required = sym.input_schema["required"].as_array().unwrap();
1857        assert!(required.iter().any(|v| v == "path"));
1858    }
1859
1860    #[test]
1861    fn test_archive_tools_definitions() {
1862        let ac = archive_create();
1863        assert_eq!(ac.name, "archive_create");
1864        assert_eq!(ac.category, ToolCategory::Archive);
1865        let required = ac.input_schema["required"].as_array().unwrap();
1866        assert!(required.iter().any(|v| v == "output_path"));
1867        assert!(required.iter().any(|v| v == "paths"));
1868
1869        let ae = archive_extract();
1870        assert_eq!(ae.name, "archive_extract");
1871        let required = ae.input_schema["required"].as_array().unwrap();
1872        assert!(required.iter().any(|v| v == "archive_path"));
1873        assert!(required.iter().any(|v| v == "destination"));
1874
1875        let al = archive_list();
1876        assert_eq!(al.name, "archive_list");
1877        let required = al.input_schema["required"].as_array().unwrap();
1878        assert!(required.iter().any(|v| v == "archive_path"));
1879    }
1880
1881    #[test]
1882    fn test_template_render_definition() {
1883        let t = template_render();
1884        assert_eq!(t.name, "template_render");
1885        assert_eq!(t.category, ToolCategory::Template);
1886        let required = t.input_schema["required"].as_array().unwrap();
1887        assert!(required.iter().any(|v| v == "template"));
1888        assert!(required.iter().any(|v| v == "variables"));
1889    }
1890
1891    #[test]
1892    fn test_crypto_tools_definitions() {
1893        let hc = hash_compute();
1894        assert_eq!(hc.name, "hash_compute");
1895        assert_eq!(hc.category, ToolCategory::Crypto);
1896
1897        let hv = hash_verify();
1898        assert_eq!(hv.name, "hash_verify");
1899        let required = hv.input_schema["required"].as_array().unwrap();
1900        assert!(required.iter().any(|v| v == "expected"));
1901    }
1902
1903    #[test]
1904    fn test_env_tools_definitions() {
1905        let eg = env_get();
1906        assert_eq!(eg.name, "env_get");
1907        assert_eq!(eg.category, ToolCategory::Shell);
1908        let required = eg.input_schema["required"].as_array().unwrap();
1909        assert!(required.iter().any(|v| v == "name"));
1910
1911        let el = env_list();
1912        assert_eq!(el.name, "env_list");
1913    }
1914
1915    #[test]
1916    fn test_text_tools_definitions() {
1917        let td = text_diff();
1918        assert_eq!(td.name, "text_diff");
1919        assert_eq!(td.category, ToolCategory::Data);
1920        let required = td.input_schema["required"].as_array().unwrap();
1921        assert!(required.iter().any(|v| v == "old_text"));
1922        assert!(required.iter().any(|v| v == "new_text"));
1923
1924        let tc = text_count();
1925        assert_eq!(tc.name, "text_count");
1926        let required = tc.input_schema["required"].as_array().unwrap();
1927        assert!(required.iter().any(|v| v == "text"));
1928    }
1929
1930    // -----------------------------------------------------------------------
1931    // tools_for_capabilities tests per capability variant
1932    // -----------------------------------------------------------------------
1933
1934    #[test]
1935    fn test_tools_for_file_read_capability() {
1936        let caps = vec![Capability::FileRead("**".into())];
1937        let tools = tools_for_capabilities(&caps);
1938        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
1939        assert!(names.contains(&"file_read"));
1940        assert!(names.contains(&"file_list"));
1941        assert!(names.contains(&"file_search"));
1942        assert!(names.contains(&"file_info"));
1943        assert!(!names.contains(&"file_write"));
1944    }
1945
1946    #[test]
1947    fn test_tools_for_file_write_capability() {
1948        let caps = vec![Capability::FileWrite("**".into())];
1949        let tools = tools_for_capabilities(&caps);
1950        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
1951        assert!(names.contains(&"file_write"));
1952        assert!(names.contains(&"patch_apply"));
1953        assert!(!names.contains(&"file_read"));
1954    }
1955
1956    #[test]
1957    fn test_tools_for_shell_exec_capability() {
1958        let caps = vec![Capability::ShellExec("*".into())];
1959        let tools = tools_for_capabilities(&caps);
1960        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
1961        assert!(names.contains(&"shell_exec"));
1962        assert!(names.contains(&"process_list"));
1963        assert!(names.contains(&"process_kill"));
1964        assert!(names.contains(&"env_get"));
1965        assert!(names.contains(&"env_list"));
1966    }
1967
1968    #[test]
1969    fn test_tools_for_network_capability() {
1970        let caps = vec![Capability::Network("*".into())];
1971        let tools = tools_for_capabilities(&caps);
1972        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
1973        assert!(names.contains(&"web_fetch"));
1974        assert!(names.contains(&"web_search"));
1975        assert!(names.contains(&"http_request"));
1976        assert!(names.contains(&"http_post"));
1977    }
1978
1979    #[test]
1980    fn test_tools_for_memory_capability() {
1981        let caps = vec![Capability::Memory];
1982        let tools = tools_for_capabilities(&caps);
1983        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
1984        assert!(names.contains(&"memory_store"));
1985        assert!(names.contains(&"memory_recall"));
1986        assert_eq!(names.len(), 2);
1987    }
1988
1989    #[test]
1990    fn test_tools_for_knowledge_graph_capability() {
1991        let caps = vec![Capability::KnowledgeGraph];
1992        let tools = tools_for_capabilities(&caps);
1993        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
1994        assert!(names.contains(&"knowledge_add_entity"));
1995        assert!(names.contains(&"knowledge_add_relation"));
1996        assert!(names.contains(&"knowledge_query"));
1997        assert_eq!(names.len(), 3);
1998    }
1999
2000    #[test]
2001    fn test_tools_for_agent_spawn_capability() {
2002        let caps = vec![Capability::AgentSpawn];
2003        let tools = tools_for_capabilities(&caps);
2004        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2005        assert!(names.contains(&"agent_spawn"));
2006        assert_eq!(names.len(), 1);
2007    }
2008
2009    #[test]
2010    fn test_tools_for_agent_message_capability() {
2011        let caps = vec![Capability::AgentMessage];
2012        let tools = tools_for_capabilities(&caps);
2013        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2014        assert!(names.contains(&"agent_message"));
2015        assert!(names.contains(&"agent_list"));
2016        assert_eq!(names.len(), 2);
2017    }
2018
2019    #[test]
2020    fn test_tools_for_source_control_capability() {
2021        let caps = vec![Capability::SourceControl];
2022        let tools = tools_for_capabilities(&caps);
2023        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2024        assert!(names.contains(&"git_status"));
2025        assert!(names.contains(&"git_diff"));
2026        assert!(names.contains(&"git_log"));
2027        assert!(names.contains(&"git_commit"));
2028        assert!(names.contains(&"git_branch"));
2029        assert_eq!(names.len(), 5);
2030    }
2031
2032    #[test]
2033    fn test_tools_for_container_capability() {
2034        let caps = vec![Capability::Container];
2035        let tools = tools_for_capabilities(&caps);
2036        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2037        assert!(names.contains(&"docker_ps"));
2038        assert!(names.contains(&"docker_run"));
2039        assert!(names.contains(&"docker_build"));
2040        assert!(names.contains(&"docker_logs"));
2041        assert_eq!(names.len(), 4);
2042    }
2043
2044    #[test]
2045    fn test_tools_for_data_manipulation_capability() {
2046        let caps = vec![Capability::DataManipulation];
2047        let tools = tools_for_capabilities(&caps);
2048        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2049        assert!(names.contains(&"json_query"));
2050        assert!(names.contains(&"json_transform"));
2051        assert!(names.contains(&"yaml_parse"));
2052        assert!(names.contains(&"regex_match"));
2053        assert!(names.contains(&"regex_replace"));
2054        assert!(names.contains(&"text_diff"));
2055        assert!(names.contains(&"text_count"));
2056        assert_eq!(names.len(), 7);
2057    }
2058
2059    #[test]
2060    fn test_tools_for_schedule_capability() {
2061        let caps = vec![Capability::Schedule];
2062        let tools = tools_for_capabilities(&caps);
2063        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2064        assert!(names.contains(&"schedule_task"));
2065        assert!(names.contains(&"schedule_list"));
2066        assert!(names.contains(&"schedule_cancel"));
2067        assert_eq!(names.len(), 3);
2068    }
2069
2070    #[test]
2071    fn test_tools_for_code_analysis_capability() {
2072        let caps = vec![Capability::CodeAnalysis];
2073        let tools = tools_for_capabilities(&caps);
2074        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2075        assert!(names.contains(&"code_search"));
2076        assert!(names.contains(&"code_symbols"));
2077        assert_eq!(names.len(), 2);
2078    }
2079
2080    #[test]
2081    fn test_tools_for_archive_capability() {
2082        let caps = vec![Capability::Archive];
2083        let tools = tools_for_capabilities(&caps);
2084        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2085        assert!(names.contains(&"archive_create"));
2086        assert!(names.contains(&"archive_extract"));
2087        assert!(names.contains(&"archive_list"));
2088        assert_eq!(names.len(), 3);
2089    }
2090
2091    #[test]
2092    fn test_tools_for_template_capability() {
2093        let caps = vec![Capability::Template];
2094        let tools = tools_for_capabilities(&caps);
2095        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2096        assert!(names.contains(&"template_render"));
2097        assert_eq!(names.len(), 1);
2098    }
2099
2100    #[test]
2101    fn test_tools_for_crypto_capability() {
2102        let caps = vec![Capability::Crypto];
2103        let tools = tools_for_capabilities(&caps);
2104        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2105        assert!(names.contains(&"hash_compute"));
2106        assert!(names.contains(&"hash_verify"));
2107        assert_eq!(names.len(), 2);
2108    }
2109
2110    #[test]
2111    fn test_tools_for_empty_capabilities() {
2112        let caps: Vec<Capability> = vec![];
2113        let tools = tools_for_capabilities(&caps);
2114        assert!(tools.is_empty());
2115    }
2116
2117    #[test]
2118    fn test_tools_for_event_publish_returns_empty() {
2119        // EventPublish is in the _ => {} catch-all
2120        let caps = vec![Capability::EventPublish];
2121        let tools = tools_for_capabilities(&caps);
2122        assert!(tools.is_empty());
2123    }
2124
2125    // -----------------------------------------------------------------------
2126    // push_unique dedup tests
2127    // -----------------------------------------------------------------------
2128
2129    #[test]
2130    fn test_push_unique_dedup() {
2131        let mut tools = Vec::new();
2132        push_unique(&mut tools, file_read());
2133        push_unique(&mut tools, file_read());
2134        push_unique(&mut tools, file_read());
2135        assert_eq!(tools.len(), 1);
2136    }
2137
2138    #[test]
2139    fn test_push_unique_different_tools() {
2140        let mut tools = Vec::new();
2141        push_unique(&mut tools, file_read());
2142        push_unique(&mut tools, file_write());
2143        push_unique(&mut tools, shell_exec());
2144        assert_eq!(tools.len(), 3);
2145    }
2146
2147    #[test]
2148    fn test_tools_for_multiple_capabilities_dedup() {
2149        // FileRead + FileRead should not double-add tools
2150        let caps = vec![
2151            Capability::FileRead("src/**".into()),
2152            Capability::FileRead("tests/**".into()),
2153        ];
2154        let tools = tools_for_capabilities(&caps);
2155        let file_read_count = tools.iter().filter(|t| t.name == "file_read").count();
2156        assert_eq!(file_read_count, 1);
2157    }
2158
2159    // -----------------------------------------------------------------------
2160    // all_tools tests
2161    // -----------------------------------------------------------------------
2162
2163    #[test]
2164    fn test_all_tools_count() {
2165        let tools = all_tools();
2166        // Count the items in the vec literal in all_tools()
2167        assert!(
2168            tools.len() >= 50,
2169            "expected at least 50 tools, got {}",
2170            tools.len()
2171        );
2172    }
2173
2174    #[test]
2175    fn test_all_tools_unique_names() {
2176        let tools = all_tools();
2177        let mut names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
2178        let original_len = names.len();
2179        names.sort();
2180        names.dedup();
2181        assert_eq!(names.len(), original_len, "all_tools has duplicate names");
2182    }
2183
2184    #[test]
2185    fn test_all_tools_valid_schemas() {
2186        let tools = all_tools();
2187        for tool in &tools {
2188            assert_eq!(
2189                tool.input_schema["type"], "object",
2190                "tool {} has non-object schema",
2191                tool.name
2192            );
2193            assert!(!tool.name.is_empty(), "tool has empty name");
2194            assert!(
2195                !tool.description.is_empty(),
2196                "tool {} has empty description",
2197                tool.name
2198            );
2199        }
2200    }
2201}