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