Skip to main content

rustyclaw_core/tools/
mod.rs

1pub mod agent_setup;
2// Agent tool system for RustyClaw.
3//
4// Provides a registry of tools that the language model can invoke, and
5// formatters that serialise the tool definitions into each provider's
6// native schema (OpenAI function-calling, Anthropic tool-use, Google
7// function declarations).
8
9use tracing::{debug, warn, instrument};
10
11mod helpers;
12mod file;
13mod runtime;
14mod web;
15mod memory_tools;
16mod cron_tool;
17mod sessions_tools;
18mod patch;
19mod gateway_tools;
20mod devices;
21mod browser;
22mod skills_tools;
23mod secrets_tools;
24mod system_tools;
25mod sysadmin;
26pub mod exo_ai;
27pub mod npm;
28pub mod ollama;
29pub mod uv;
30// UV tool
31use uv::exec_uv_manage;
32
33// npm / Node.js tool
34use npm::exec_npm_manage;
35
36// Agent setup orchestrator
37use agent_setup::exec_agent_setup;
38mod params;
39
40// Re-export helpers for external use
41pub use helpers::{
42    process_manager, set_credentials_dir, is_protected_path,
43    expand_tilde, VAULT_ACCESS_DENIED, command_references_credentials,
44    init_sandbox, sandbox, run_sandboxed_command,
45    set_vault, vault, SharedVault,
46    sanitize_tool_output,
47};
48
49// File operations
50use file::{exec_read_file, exec_write_file, exec_edit_file, exec_list_directory, exec_search_files, exec_find_files};
51
52// Runtime operations
53use runtime::{exec_execute_command, exec_process};
54
55// Web operations
56use web::{exec_web_fetch, exec_web_search};
57
58// Memory operations
59use memory_tools::{exec_memory_search, exec_memory_get};
60
61// Cron operations
62use cron_tool::exec_cron;
63
64// Session operations
65use sessions_tools::{exec_sessions_list, exec_sessions_spawn, exec_sessions_send, exec_sessions_history, exec_session_status, exec_agents_list};
66
67// Patch operations
68use patch::exec_apply_patch;
69
70// Gateway operations
71use gateway_tools::{exec_gateway, exec_message, exec_tts, exec_image};
72
73// Device operations
74use devices::{exec_nodes, exec_canvas};
75
76// Browser automation (separate module with feature-gated implementation)
77use browser::exec_browser;
78
79// Skill operations
80use skills_tools::{exec_skill_list, exec_skill_search, exec_skill_install, exec_skill_info, exec_skill_enable, exec_skill_link_secret, exec_skill_create};
81
82// Secrets operations
83use secrets_tools::exec_secrets_stub;
84
85// System tools
86use system_tools::{
87    exec_disk_usage, exec_classify_files, exec_system_monitor,
88    exec_battery_health, exec_app_index, exec_cloud_browse,
89    exec_browser_cache, exec_screenshot, exec_clipboard,
90    exec_audit_sensitive, exec_secure_delete, exec_summarize_file,
91};
92
93// System administration tools
94use sysadmin::{
95    exec_pkg_manage, exec_net_info, exec_net_scan,
96    exec_service_manage, exec_user_manage, exec_firewall,
97};
98
99// Exo AI tools
100use exo_ai::exec_exo_manage;
101
102// Ollama tools
103use ollama::exec_ollama_manage;
104
105/// Stub executor for the `ask_user` tool — never called directly.
106/// Execution is intercepted by the gateway, which forwards the prompt
107/// to the TUI and returns the user's response as the tool result.
108fn exec_ask_user_stub(_args: &Value, _workspace_dir: &Path) -> Result<String, String> {
109    Err("ask_user must be executed via the gateway".into())
110}
111
112use serde::{Deserialize, Serialize};
113use serde_json::{json, Value};
114use std::path::Path;
115
116// ── Tool permissions ────────────────────────────────────────────────────────
117
118/// Permission level for a tool, controlling whether the agent can invoke it.
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120#[serde(rename_all = "snake_case")]
121pub enum ToolPermission {
122    /// Tool is always allowed — no confirmation needed.
123    Allow,
124    /// Tool is always denied — the model receives an error.
125    Deny,
126    /// Tool requires user confirmation each time.
127    Ask,
128    /// Tool is only allowed when invoked by a named skill.
129    SkillOnly(Vec<String>),
130}
131
132impl Default for ToolPermission {
133    fn default() -> Self {
134        Self::Allow
135    }
136}
137
138impl std::fmt::Display for ToolPermission {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        match self {
141            Self::Allow => write!(f, "Allow"),
142            Self::Deny => write!(f, "Deny"),
143            Self::Ask => write!(f, "Ask"),
144            Self::SkillOnly(skills) => {
145                if skills.is_empty() {
146                    write!(f, "Skill Only (none)")
147                } else {
148                    write!(f, "Skill Only ({})", skills.join(", "))
149                }
150            }
151        }
152    }
153}
154
155impl ToolPermission {
156    /// Cycle to the next simple permission level (for UI toggle).
157    /// SkillOnly is accessed via a separate edit flow.
158    pub fn cycle(&self) -> Self {
159        match self {
160            Self::Allow => Self::Ask,
161            Self::Ask => Self::Deny,
162            Self::Deny => Self::SkillOnly(Vec::new()),
163            Self::SkillOnly(_) => Self::Allow,
164        }
165    }
166
167    /// Short badge text for the TUI.
168    pub fn badge(&self) -> &'static str {
169        match self {
170            Self::Allow => "ALLOW",
171            Self::Deny => "DENY",
172            Self::Ask => "ASK",
173            Self::SkillOnly(_) => "SKILL",
174        }
175    }
176
177    /// Human-readable description of what this permission level does.
178    pub fn description(&self) -> &'static str {
179        match self {
180            Self::Allow => "Tool runs automatically — no confirmation needed.",
181            Self::Deny => "Tool is blocked — the model receives an error and cannot use it.",
182            Self::Ask => "You will be prompted to approve or deny each use of this tool.",
183            Self::SkillOnly(_) => "Tool can only be used by named skills, not in direct chat.",
184        }
185    }
186}
187
188/// Return all tool names as a sorted list.
189pub fn all_tool_names() -> Vec<&'static str> {
190    let mut names: Vec<&'static str> = all_tools().iter().map(|t| t.name).collect();
191    names.sort();
192    names
193}
194
195/// Short, user-facing summary of what each tool lets the agent do.
196pub fn tool_summary(name: &str) -> &'static str {
197    match name {
198        "read_file" => "Read files on your computer",
199        "write_file" => "Create or overwrite files",
200        "edit_file" => "Edit existing files",
201        "list_directory" => "List folder contents",
202        "search_files" => "Search inside file contents",
203        "find_files" => "Find files by name",
204        "execute_command" => "Run shell commands",
205        "web_fetch" => "Fetch content from URLs",
206        "web_search" => "Search the web",
207        "process" => "Manage background processes",
208        "memory_search" => "Search agent memory files",
209        "memory_get" => "Read agent memory files",
210        "cron" => "Manage scheduled jobs",
211        "sessions_list" => "List active sessions",
212        "sessions_spawn" => "Spawn sub-agent sessions",
213        "sessions_send" => "Send messages to sessions",
214        "sessions_history" => "Read session message history",
215        "session_status" => "Check session status & usage",
216        "agents_list" => "List available agent types",
217        "apply_patch" => "Apply diff patches to files",
218        "secrets_list" => "List vault secret names",
219        "secrets_get" => "Read secrets from the vault",
220        "secrets_store" => "Store secrets in the vault",
221        "gateway" => "Control the gateway daemon",
222        "message" => "Send messages via channels",
223        "tts" => "Convert text to speech",
224        "image" => "Analyze images with vision AI",
225        "nodes" => "Control paired companion devices",
226        "browser" => "Automate a web browser",
227        "canvas" => "Display UI on node canvases",
228        "skill_list" => "List loaded skills",
229        "skill_search" => "Search the skill registry",
230        "skill_install" => "Install skills from registry",
231        "skill_info" => "View skill details",
232        "skill_enable" => "Enable or disable skills",
233        "skill_link_secret" => "Link vault secrets to skills",
234        "skill_create" => "Create a new skill from scratch",
235        "disk_usage" => "Scan disk usage by folder",
236        "classify_files" => "Categorize files as docs, caches, etc.",
237        "system_monitor" => "View CPU, memory & process info",
238        "battery_health" => "Check battery status & health",
239        "app_index" => "List installed apps by size",
240        "cloud_browse" => "Browse local cloud storage folders",
241        "browser_cache" => "Audit or clean browser caches",
242        "screenshot" => "Capture a screenshot",
243        "clipboard" => "Read or write the clipboard",
244        "audit_sensitive" => "Scan files for exposed secrets",
245        "secure_delete" => "Securely overwrite & delete files",
246        "summarize_file" => "Preview-summarize any file type",
247        "ask_user" => "Ask the user structured questions",
248        "ollama_manage" => "Administer the Ollama model server",
249        "exo_manage" => "Administer the Exo distributed AI cluster (git clone + uv run)",
250        "uv_manage" => "Manage Python envs & packages via uv",
251        "npm_manage" => "Manage Node.js packages & scripts via npm",
252        "agent_setup" => "Set up local model infrastructure",
253        _ => "Unknown tool",
254    }
255}
256
257// ── Tool definitions ────────────────────────────────────────────────────────
258
259/// JSON-Schema-like parameter definition.
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct ToolParam {
262    pub name: String,
263    pub description: String,
264    /// JSON Schema type: "string", "integer", "boolean", "array", "object".
265    #[serde(rename = "type")]
266    pub param_type: String,
267    pub required: bool,
268}
269
270/// A tool that the agent can invoke.
271#[derive(Debug, Clone)]
272pub struct ToolDef {
273    pub name: &'static str,
274    pub description: &'static str,
275    pub parameters: Vec<ToolParam>,
276    /// The function that executes the tool, returning a string result or error.
277    pub execute: fn(args: &Value, workspace_dir: &Path) -> Result<String, String>,
278}
279
280// ── Tool registry ───────────────────────────────────────────────────────────
281
282/// Return all available tools.
283pub fn all_tools() -> Vec<&'static ToolDef> {
284    vec![
285        &READ_FILE,
286        &WRITE_FILE,
287        &EDIT_FILE,
288        &LIST_DIRECTORY,
289        &SEARCH_FILES,
290        &FIND_FILES,
291        &EXECUTE_COMMAND,
292        &WEB_FETCH,
293        &WEB_SEARCH,
294        &PROCESS,
295        &MEMORY_SEARCH,
296        &MEMORY_GET,
297        &CRON,
298        &SESSIONS_LIST,
299        &SESSIONS_SPAWN,
300        &SESSIONS_SEND,
301        &SESSIONS_HISTORY,
302        &SESSION_STATUS,
303        &AGENTS_LIST,
304        &APPLY_PATCH,
305        &SECRETS_LIST,
306        &SECRETS_GET,
307        &SECRETS_STORE,
308        &GATEWAY,
309        &MESSAGE,
310        &TTS,
311        &IMAGE,
312        &NODES,
313        &BROWSER,
314        &CANVAS,
315        &SKILL_LIST,
316        &SKILL_SEARCH,
317        &SKILL_INSTALL,
318        &SKILL_INFO,
319        &SKILL_ENABLE,
320        &SKILL_LINK_SECRET,
321        &SKILL_CREATE,
322        &DISK_USAGE,
323        &CLASSIFY_FILES,
324        &SYSTEM_MONITOR,
325        &BATTERY_HEALTH,
326        &APP_INDEX,
327        &CLOUD_BROWSE,
328        &BROWSER_CACHE,
329        &SCREENSHOT,
330        &CLIPBOARD,
331        &AUDIT_SENSITIVE,
332        &SECURE_DELETE,
333        &SUMMARIZE_FILE,
334        &PKG_MANAGE,
335        &NET_INFO,
336        &NET_SCAN,
337        &SERVICE_MANAGE,
338        &USER_MANAGE,
339        &FIREWALL,
340        &OLLAMA_MANAGE,
341        &EXO_MANAGE,
342        &UV_MANAGE,
343        &NPM_MANAGE,
344        &AGENT_SETUP,
345        &ASK_USER,
346    ]
347}
348
349// ── Built-in tools ──────────────────────────────────────────────────────────
350
351/// `read_file` — read the contents of a file on disk.
352pub static READ_FILE: ToolDef = ToolDef {
353    name: "read_file",
354    description: "Read the contents of a file. Returns the file text. \
355                  Handles plain text files directly and can also extract \
356                  text from .docx, .doc, .rtf, .odt, .pdf, and .html files. \
357                  If you have an absolute path from find_files or search_files, \
358                  pass it exactly as-is. Use the optional start_line / end_line \
359                  parameters to read a specific range (1-based, inclusive).",
360    parameters: vec![],  // filled by init; see `read_file_params()`.
361    execute: exec_read_file,
362};
363
364pub static WRITE_FILE: ToolDef = ToolDef {
365    name: "write_file",
366    description: "Create or overwrite a file with the given content. \
367                  Parent directories are created automatically.",
368    parameters: vec![],
369    execute: exec_write_file,
370};
371
372pub static EDIT_FILE: ToolDef = ToolDef {
373    name: "edit_file",
374    description: "Make a targeted edit to an existing file using search-and-replace. \
375                  The old_string must match exactly one location in the file. \
376                  Include enough context lines to make the match unique.",
377    parameters: vec![],
378    execute: exec_edit_file,
379};
380
381pub static LIST_DIRECTORY: ToolDef = ToolDef {
382    name: "list_directory",
383    description: "List the contents of a directory. Returns file and \
384                  directory names, with directories suffixed by '/'.",
385    parameters: vec![],
386    execute: exec_list_directory,
387};
388
389pub static SEARCH_FILES: ToolDef = ToolDef {
390    name: "search_files",
391    description: "Search file CONTENTS for a text pattern (like grep -i). \
392                  The search is case-insensitive. Returns matching lines \
393                  with paths and line numbers. Use `find_files` instead \
394                  when searching by file name. Set `path` to an absolute \
395                  directory (e.g. '/Users/alice') to search outside the \
396                  workspace.",
397    parameters: vec![],
398    execute: exec_search_files,
399};
400
401pub static FIND_FILES: ToolDef = ToolDef {
402    name: "find_files",
403    description: "Find files by name. Returns paths that can be passed directly to read_file. Accepts plain keywords (case-insensitive \
404                  substring match) OR glob patterns (e.g. '*.pdf'). Multiple \
405                  keywords can be separated with spaces — a file matches if its \
406                  name contains ANY keyword. Examples: 'resume', 'resume cv', \
407                  '*.pdf'. Set `path` to an absolute directory to search outside \
408                  the workspace (e.g. '/Users/alice'). Use `search_files` to \
409                  search file CONTENTS instead.",
410    parameters: vec![],
411    execute: exec_find_files,
412};
413
414pub static EXECUTE_COMMAND: ToolDef = ToolDef {
415    name: "execute_command",
416    description: "Execute a shell command and return its output (stdout + stderr). \
417                  Runs via `sh -c` in the workspace directory by default. \
418                  Use for builds, tests, git operations, system lookups \
419                  (e.g. `find ~ -name '*.pdf'`, `mdfind`, `which`), or \
420                  any other CLI task. Set `working_dir` to an absolute \
421                  path to run in a different directory.",
422    parameters: vec![],
423    execute: exec_execute_command,
424};
425
426pub static WEB_FETCH: ToolDef = ToolDef {
427    name: "web_fetch",
428    description: "Fetch and extract readable content from a URL (HTML → markdown or plain text). \
429                  Use for reading web pages, documentation, articles, or any HTTP-accessible content. \
430                  Set use_cookies=true to use stored browser cookies for authenticated requests. \
431                  For JavaScript-heavy sites that require rendering, use a browser tool instead.",
432    parameters: vec![],
433    execute: exec_web_fetch,
434};
435
436pub static WEB_SEARCH: ToolDef = ToolDef {
437    name: "web_search",
438    description: "Search the web using Brave Search API. Returns titles, URLs, and snippets. \
439                  Requires BRAVE_API_KEY environment variable to be set. \
440                  Use for finding current information, research, and fact-checking.",
441    parameters: vec![],
442    execute: exec_web_search,
443};
444
445pub static PROCESS: ToolDef = ToolDef {
446    name: "process",
447    description: "Manage background exec sessions. Actions: list (show all sessions), \
448                  poll (get new output + status for a session), log (get output with offset/limit), \
449                  write (send data to stdin), kill (terminate a session), clear (remove completed sessions), \
450                  remove (remove a specific session).",
451    parameters: vec![],
452    execute: exec_process,
453};
454
455pub static MEMORY_SEARCH: ToolDef = ToolDef {
456    name: "memory_search",
457    description: "Semantically search MEMORY.md and memory/*.md files for relevant information. \
458                  Use before answering questions about prior work, decisions, dates, people, \
459                  preferences, or todos. Returns matching snippets with file path and line numbers.",
460    parameters: vec![],
461    execute: exec_memory_search,
462};
463
464pub static MEMORY_GET: ToolDef = ToolDef {
465    name: "memory_get",
466    description: "Read content from a memory file (MEMORY.md or memory/*.md). \
467                  Use after memory_search to get full context around a snippet. \
468                  Supports optional line range for large files.",
469    parameters: vec![],
470    execute: exec_memory_get,
471};
472
473pub static CRON: ToolDef = ToolDef {
474    name: "cron",
475    description: "Manage scheduled jobs. Actions: status (scheduler status), list (show jobs), \
476                  add (create job), update (modify job), remove (delete job), run (trigger immediately), \
477                  runs (get run history). Use for reminders and recurring tasks.",
478    parameters: vec![],
479    execute: exec_cron,
480};
481
482pub static SESSIONS_LIST: ToolDef = ToolDef {
483    name: "sessions_list",
484    description: "List active sessions with optional filters. Shows main sessions and sub-agents. \
485                  Use to check on running background tasks.",
486    parameters: vec![],
487    execute: exec_sessions_list,
488};
489
490pub static SESSIONS_SPAWN: ToolDef = ToolDef {
491    name: "sessions_spawn",
492    description: "Spawn a sub-agent to run a task in the background. The sub-agent runs in its own \
493                  isolated session and announces results back when finished. Non-blocking.",
494    parameters: vec![],
495    execute: exec_sessions_spawn,
496};
497
498pub static SESSIONS_SEND: ToolDef = ToolDef {
499    name: "sessions_send",
500    description: "Send a message to another session. Use sessionKey or label to identify the target. \
501                  Returns immediately after sending.",
502    parameters: vec![],
503    execute: exec_sessions_send,
504};
505
506pub static SESSIONS_HISTORY: ToolDef = ToolDef {
507    name: "sessions_history",
508    description: "Fetch message history for a session. Returns recent messages from the specified session.",
509    parameters: vec![],
510    execute: exec_sessions_history,
511};
512
513pub static SESSION_STATUS: ToolDef = ToolDef {
514    name: "session_status",
515    description: "Show session status including usage, time, and cost. Use for model-use questions. \
516                  Can also set per-session model override.",
517    parameters: vec![],
518    execute: exec_session_status,
519};
520
521pub static AGENTS_LIST: ToolDef = ToolDef {
522    name: "agents_list",
523    description: "List available agent IDs that can be targeted with sessions_spawn. \
524                  Returns the configured agents based on allowlists.",
525    parameters: vec![],
526    execute: exec_agents_list,
527};
528
529pub static APPLY_PATCH: ToolDef = ToolDef {
530    name: "apply_patch",
531    description: "Apply a unified diff patch to one or more files. Supports multi-hunk patches. \
532                  Use for complex multi-line edits where edit_file would be cumbersome.",
533    parameters: vec![],
534    execute: exec_apply_patch,
535};
536
537pub static SECRETS_LIST: ToolDef = ToolDef {
538    name: "secrets_list",
539    description: "List the names (keys) stored in the encrypted secrets vault. \
540                  Returns only key names, never values. Use secrets_get to \
541                  retrieve a specific value.",
542    parameters: vec![],
543    execute: exec_secrets_stub,
544};
545
546pub static SECRETS_GET: ToolDef = ToolDef {
547    name: "secrets_get",
548    description: "Retrieve a secret value from the encrypted vault by key name. \
549                  The value is returned as a string. Prefer injecting it directly \
550                  into environment variables or config rather than echoing it.",
551    parameters: vec![],
552    execute: exec_secrets_stub,
553};
554
555pub static SECRETS_STORE: ToolDef = ToolDef {
556    name: "secrets_store",
557    description: "Store or update a key/value pair in the encrypted secrets vault. \
558                  The value is encrypted at rest. Use for API keys, tokens, and \
559                  other sensitive material.",
560    parameters: vec![],
561    execute: exec_secrets_stub,
562};
563
564pub static GATEWAY: ToolDef = ToolDef {
565    name: "gateway",
566    description: "Manage the gateway daemon. Actions: restart (restart gateway), \
567                  config.get (get current config), config.schema (get config schema), \
568                  config.apply (replace entire config), config.patch (partial config update), \
569                  update.run (update gateway).",
570    parameters: vec![],
571    execute: exec_gateway,
572};
573
574pub static MESSAGE: ToolDef = ToolDef {
575    name: "message",
576    description: "Send messages via channel plugins. Actions: send (send a message), \
577                  broadcast (send to multiple targets). Supports various channels \
578                  like telegram, discord, whatsapp, signal, etc.",
579    parameters: vec![],
580    execute: exec_message,
581};
582
583pub static TTS: ToolDef = ToolDef {
584    name: "tts",
585    description: "Convert text to speech and return a media path. Use when the user \
586                  requests audio or TTS is enabled.",
587    parameters: vec![],
588    execute: exec_tts,
589};
590
591pub static IMAGE: ToolDef = ToolDef {
592    name: "image",
593    description: "Analyze an image using the configured image/vision model. \
594                  Pass a local file path or URL. Returns a text description or \
595                  answers the prompt about the image.",
596    parameters: vec![],
597    execute: exec_image,
598};
599
600pub static NODES: ToolDef = ToolDef {
601    name: "nodes",
602    description: "Discover and control paired nodes (companion devices). Actions: \
603                  status (list nodes), describe (node details), pending/approve/reject (pairing), \
604                  notify (send notification), camera_snap/camera_list (camera), \
605                  screen_record (screen capture), location_get (GPS), run/invoke (remote commands).",
606    parameters: vec![],
607    execute: exec_nodes,
608};
609
610pub static BROWSER: ToolDef = ToolDef {
611    name: "browser",
612    description: "Control web browser for automation. Actions: status, start, stop, \
613                  profiles, tabs, open, focus, close, snapshot, screenshot, navigate, \
614                  console, pdf, act (click/type/press/hover/drag). Use snapshot to get \
615                  page accessibility tree for element targeting.",
616    parameters: vec![],
617    execute: exec_browser,
618};
619
620pub static CANVAS: ToolDef = ToolDef {
621    name: "canvas",
622    description: "Control node canvases for UI presentation. Actions: present (show content), \
623                  hide, navigate, eval (run JavaScript), snapshot (capture rendered UI), \
624                  a2ui_push/a2ui_reset (accessibility-to-UI).",
625    parameters: vec![],
626    execute: exec_canvas,
627};
628
629pub static SKILL_LIST: ToolDef = ToolDef {
630    name: "skill_list",
631    description: "List all loaded skills with their status (enabled, gates, source, linked secrets). \
632                  Use to discover what capabilities are available.",
633    parameters: vec![],
634    execute: exec_skill_list,
635};
636
637pub static SKILL_SEARCH: ToolDef = ToolDef {
638    name: "skill_search",
639    description: "Search the ClawHub registry for installable skills. Returns skill names, \
640                  descriptions, versions, and required secrets.",
641    parameters: vec![],
642    execute: exec_skill_search,
643};
644
645pub static SKILL_INSTALL: ToolDef = ToolDef {
646    name: "skill_install",
647    description: "Install a skill from the ClawHub registry by name. Optionally specify a version. \
648                  After installation the skill is immediately available. Use skill_link_secret to \
649                  bind required credentials.",
650    parameters: vec![],
651    execute: exec_skill_install,
652};
653
654pub static SKILL_INFO: ToolDef = ToolDef {
655    name: "skill_info",
656    description: "Show detailed information about a loaded skill: description, source, linked \
657                  secrets, gating status, and instructions summary.",
658    parameters: vec![],
659    execute: exec_skill_info,
660};
661
662pub static SKILL_ENABLE: ToolDef = ToolDef {
663    name: "skill_enable",
664    description: "Enable or disable a loaded skill. Disabled skills are not injected into the \
665                  agent prompt and cannot be activated.",
666    parameters: vec![],
667    execute: exec_skill_enable,
668};
669
670pub static SKILL_LINK_SECRET: ToolDef = ToolDef {
671    name: "skill_link_secret",
672    description: "Link or unlink a vault credential to a skill. When linked, the secret is \
673                  accessible under the SkillOnly policy while the skill is active. Use action \
674                  'link' to bind or 'unlink' to remove the binding.",
675    parameters: vec![],
676    execute: exec_skill_link_secret,
677};
678
679pub static SKILL_CREATE: ToolDef = ToolDef {
680    name: "skill_create",
681    description: "Create a new skill on disk. Provide a name (kebab-case), a one-line \
682                  description, and the full markdown instructions body. The skill directory \
683                  and SKILL.md file are created automatically and the skill is immediately \
684                  available for use.",
685    parameters: vec![],
686    execute: exec_skill_create,
687};
688
689
690// ── System tools ────────────────────────────────────────────────────────────
691
692pub static DISK_USAGE: ToolDef = ToolDef {
693    name: "disk_usage",
694    description: "Scan disk usage for a directory tree. Returns the largest entries \
695                  sorted by size. Defaults to the home directory. Use `depth` to \
696                  control how deep to scan and `top` to limit results.",
697    parameters: vec![],
698    execute: exec_disk_usage,
699};
700
701pub static CLASSIFY_FILES: ToolDef = ToolDef {
702    name: "classify_files",
703    description: "Classify files in a directory as user documents, caches, logs, \
704                  build artifacts, cloud storage, images, video, audio, archives, \
705                  installers, or app config. Useful for understanding what's in a folder.",
706    parameters: vec![],
707    execute: exec_classify_files,
708};
709
710pub static SYSTEM_MONITOR: ToolDef = ToolDef {
711    name: "system_monitor",
712    description: "Return current system resource usage: CPU load, memory, disk space, \
713                  network, and top processes. Use `metric` to query a specific area \
714                  or 'all' for everything.",
715    parameters: vec![],
716    execute: exec_system_monitor,
717};
718
719pub static BATTERY_HEALTH: ToolDef = ToolDef {
720    name: "battery_health",
721    description: "Report battery status including charge level, cycle count, capacity, \
722                  temperature, and charging state. Works on macOS and Linux laptops.",
723    parameters: vec![],
724    execute: exec_battery_health,
725};
726
727pub static APP_INDEX: ToolDef = ToolDef {
728    name: "app_index",
729    description: "List installed applications with their size, version, and source \
730                  (native or Homebrew). Sort by size or name. Filter by substring.",
731    parameters: vec![],
732    execute: exec_app_index,
733};
734
735pub static CLOUD_BROWSE: ToolDef = ToolDef {
736    name: "cloud_browse",
737    description: "Detect and browse local cloud storage sync folders (Google Drive, \
738                  Dropbox, OneDrive, iCloud). Use 'detect' to find them or 'list' \
739                  to browse files in a specific cloud folder.",
740    parameters: vec![],
741    execute: exec_cloud_browse,
742};
743
744pub static BROWSER_CACHE: ToolDef = ToolDef {
745    name: "browser_cache",
746    description: "Audit or clean browser cache and download folders. Supports Chrome, \
747                  Firefox, Safari, Edge, and Arc. Use 'scan' to see sizes or 'clean' \
748                  to remove cache data.",
749    parameters: vec![],
750    execute: exec_browser_cache,
751};
752
753pub static SCREENSHOT: ToolDef = ToolDef {
754    name: "screenshot",
755    description: "Capture a screenshot of the full screen or a specific region. \
756                  Supports optional delay. Saves as PNG. Uses screencapture on macOS \
757                  or imagemagick on Linux.",
758    parameters: vec![],
759    execute: exec_screenshot,
760};
761
762pub static CLIPBOARD: ToolDef = ToolDef {
763    name: "clipboard",
764    description: "Read from or write to the system clipboard. Uses pbcopy/pbpaste \
765                  on macOS or xclip/xsel on Linux.",
766    parameters: vec![],
767    execute: exec_clipboard,
768};
769
770pub static AUDIT_SENSITIVE: ToolDef = ToolDef {
771    name: "audit_sensitive",
772    description: "Scan source files for potentially sensitive data: AWS keys, private \
773                  keys, GitHub tokens, API keys, passwords, JWTs, Slack tokens. \
774                  Matches are redacted in output. Use for security reviews.",
775    parameters: vec![],
776    execute: exec_audit_sensitive,
777};
778
779pub static SECURE_DELETE: ToolDef = ToolDef {
780    name: "secure_delete",
781    description: "Securely overwrite and delete a file or directory. Overwrites with \
782                  random data multiple passes before unlinking. Requires confirm=true \
783                  to proceed (first call returns file info for review). Refuses \
784                  critical system paths.",
785    parameters: vec![],
786    execute: exec_secure_delete,
787};
788
789pub static SUMMARIZE_FILE: ToolDef = ToolDef {
790    name: "summarize_file",
791    description: "Generate a preview summary of any file: text files get head/tail and \
792                  definition extraction; PDFs get page count and text preview; images \
793                  get dimensions; media gets duration and codecs; archives get content \
794                  listing. Returns structured metadata.",
795    parameters: vec![],
796    execute: exec_summarize_file,
797};
798
799// ── System administration tools ─────────────────────────────────────────────
800
801pub static PKG_MANAGE: ToolDef = ToolDef {
802    name: "pkg_manage",
803    description: "Install, uninstall, upgrade, search, and list software packages. \
804                  Auto-detects the system package manager (brew, apt, dnf, pacman, \
805                  zypper, apk, snap, flatpak, port, nix-env) or use the manager \
806                  parameter to override. Also supports querying package info and \
807                  listing installed packages.",
808    parameters: vec![],
809    execute: exec_pkg_manage,
810};
811
812pub static NET_INFO: ToolDef = ToolDef {
813    name: "net_info",
814    description: "Query network information: interfaces, active connections, routing \
815                  table, DNS lookups, ping, traceroute, whois, ARP table, public IP, \
816                  Wi-Fi details, and bandwidth statistics.",
817    parameters: vec![],
818    execute: exec_net_info,
819};
820
821pub static NET_SCAN: ToolDef = ToolDef {
822    name: "net_scan",
823    description: "Network scanning and packet capture: run nmap scans (quick, full, \
824                  service, OS, UDP, vuln, ping, stealth), capture packets with tcpdump, \
825                  check if a specific port is open, listen for connections, sniff \
826                  traffic summaries, and discover hosts on the local network.",
827    parameters: vec![],
828    execute: exec_net_scan,
829};
830
831pub static SERVICE_MANAGE: ToolDef = ToolDef {
832    name: "service_manage",
833    description: "Manage system services: list running/loaded services, check status, \
834                  start, stop, restart, enable, disable, and view logs. Auto-detects \
835                  the init system (systemd, launchd, sysvinit).",
836    parameters: vec![],
837    execute: exec_service_manage,
838};
839
840pub static USER_MANAGE: ToolDef = ToolDef {
841    name: "user_manage",
842    description: "Manage system users and groups: whoami, list users, list groups, \
843                  get user info, add/remove users, add user to group, and view last \
844                  login history.",
845    parameters: vec![],
846    execute: exec_user_manage,
847};
848
849pub static FIREWALL: ToolDef = ToolDef {
850    name: "firewall",
851    description: "Manage the system firewall: check status, list rules, allow or deny \
852                  a port (TCP/UDP), enable or disable the firewall. Auto-detects the \
853                  firewall backend (pf, ufw, firewalld, iptables, nftables).",
854    parameters: vec![],
855    execute: exec_firewall,
856};
857
858// ── Local model & environment tools ────────────────────────────────────────
859
860pub static OLLAMA_MANAGE: ToolDef = ToolDef {
861    name: "ollama_manage",
862    description: "Administer the Ollama local model server. Actions: setup (install \
863                  ollama), serve/start, stop, status, pull/add (download a model), \
864                  rm/remove (delete a model), list (show downloaded models), \
865                  show/info (model details), ps/running (loaded models), \
866                  load/warm (preload into VRAM), unload/evict (free VRAM), \
867                  copy/cp (duplicate a model tag).",
868    parameters: vec![],
869    execute: exec_ollama_manage,
870};
871
872pub static EXO_MANAGE: ToolDef = ToolDef {
873    name: "exo_manage",
874    description: "Administer the Exo distributed AI inference cluster (exo-explore/exo). \
875                  Actions: setup (clone repo, install prereqs, build dashboard), \
876                  start/run (launch exo node), stop, status (cluster overview with download \
877                  progress), models/list (available models), state/topology (cluster nodes, \
878                  instances & downloads), downloads/progress (show model download status with \
879                  progress bars), preview (placement previews for a model), load/add/pull \
880                  (create model instance / start download), unload/remove (delete instance), \
881                  update (git pull + rebuild), log (view logs).",
882    parameters: vec![],
883    execute: exec_exo_manage,
884};
885
886pub static UV_MANAGE: ToolDef = ToolDef {
887    name: "uv_manage",
888    description: "Manage Python environments and packages via uv (ultra-fast package \
889                  manager). Actions: setup (install uv), version, venv (create virtualenv), \
890                  pip-install/add (install packages), pip-uninstall/remove (uninstall), \
891                  pip-list/list (show installed), pip-freeze/freeze (export requirements), \
892                  sync (install from requirements), run (execute in env), python \
893                  (install a Python version), init (create new project).",
894    parameters: vec![],
895    execute: exec_uv_manage,
896};
897
898pub static NPM_MANAGE: ToolDef = ToolDef {
899    name: "npm_manage",
900    description: "Manage Node.js packages and scripts via npm. Actions: setup \
901                  (install Node.js/npm), version, init (create package.json), \
902                  npm-install/add (install packages), uninstall/remove, list, \
903                  outdated, update, run (run a script), start, build, test, \
904                  npx/exec (run a package binary), audit, cache-clean, info, \
905                  search, status.",
906    parameters: vec![],
907    execute: exec_npm_manage,
908};
909
910pub static AGENT_SETUP: ToolDef = ToolDef {
911    name: "agent_setup",
912    description: "Set up the local model infrastructure in one command. Installs and \
913                  verifies uv (Python package manager), exo (distributed AI cluster), \
914                  and ollama (local model server). Use the optional 'components' \
915                  parameter to set up only specific tools (e.g. ['ollama','uv']).",
916    parameters: vec![],
917    execute: exec_agent_setup,
918};
919
920// ── Interactive prompt tool ────────────────────────────────────────────────
921
922pub static ASK_USER: ToolDef = ToolDef {
923    name: "ask_user",
924    description: "Ask the user a structured question. Opens an interactive dialog \
925                  in the TUI for the user to respond. Supports five prompt types: \
926                  'select' (pick one from a list), 'multi_select' (pick multiple), \
927                  'confirm' (yes/no), 'text' (free text input), and 'form' \
928                  (multiple named fields). Returns the user's answer as a JSON value. \
929                  Use this when you need specific, structured input rather than free chat.",
930    parameters: vec![],
931    execute: exec_ask_user_stub,
932};
933
934// Re-export parameter functions from params module
935pub use params::*;
936
937// ── Provider-specific formatters ────────────────────────────────────────────
938
939/// Parameters for a tool, building a JSON Schema `properties` / `required`.
940fn params_to_json_schema(params: &[ToolParam]) -> (Value, Value) {
941    let mut properties = serde_json::Map::new();
942    let mut required = Vec::new();
943
944    for p in params {
945        let mut prop = serde_json::Map::new();
946        prop.insert("type".into(), json!(p.param_type));
947        prop.insert("description".into(), json!(p.description));
948
949        // Arrays need an items schema
950        if p.param_type == "array" {
951            prop.insert("items".into(), json!({"type": "string"}));
952        }
953
954        properties.insert(p.name.clone(), Value::Object(prop));
955        if p.required {
956            required.push(json!(p.name));
957        }
958    }
959
960    (Value::Object(properties), Value::Array(required))
961}
962
963/// Resolve the parameter list for a tool (static defs use empty vecs
964/// because Vec isn't const; we resolve at call time).
965fn resolve_params(tool: &ToolDef) -> Vec<ToolParam> {
966    if !tool.parameters.is_empty() {
967        return tool.parameters.clone();
968    }
969    match tool.name {
970        "read_file" => read_file_params(),
971        "write_file" => write_file_params(),
972        "edit_file" => edit_file_params(),
973        "list_directory" => list_directory_params(),
974        "search_files" => search_files_params(),
975        "find_files" => find_files_params(),
976        "execute_command" => execute_command_params(),
977        "web_fetch" => web_fetch_params(),
978        "web_search" => web_search_params(),
979        "process" => process_params(),
980        "memory_search" => memory_search_params(),
981        "memory_get" => memory_get_params(),
982        "cron" => cron_params(),
983        "sessions_list" => sessions_list_params(),
984        "sessions_spawn" => sessions_spawn_params(),
985        "sessions_send" => sessions_send_params(),
986        "sessions_history" => sessions_history_params(),
987        "session_status" => session_status_params(),
988        "agents_list" => agents_list_params(),
989        "apply_patch" => apply_patch_params(),
990        "secrets_list" => secrets_list_params(),
991        "secrets_get" => secrets_get_params(),
992        "secrets_store" => secrets_store_params(),
993        "gateway" => gateway_params(),
994        "message" => message_params(),
995        "tts" => tts_params(),
996        "image" => image_params(),
997        "nodes" => nodes_params(),
998        "browser" => browser_params(),
999        "canvas" => canvas_params(),
1000        "skill_list" => skill_list_params(),
1001        "skill_search" => skill_search_params(),
1002        "skill_install" => skill_install_params(),
1003        "skill_info" => skill_info_params(),
1004        "skill_enable" => skill_enable_params(),
1005        "skill_link_secret" => skill_link_secret_params(),
1006        "skill_create" => skill_create_params(),
1007        "disk_usage" => disk_usage_params(),
1008        "classify_files" => classify_files_params(),
1009        "system_monitor" => system_monitor_params(),
1010        "battery_health" => battery_health_params(),
1011        "app_index" => app_index_params(),
1012        "cloud_browse" => cloud_browse_params(),
1013        "browser_cache" => browser_cache_params(),
1014        "screenshot" => screenshot_params(),
1015        "clipboard" => clipboard_params(),
1016        "audit_sensitive" => audit_sensitive_params(),
1017        "secure_delete" => secure_delete_params(),
1018        "summarize_file" => summarize_file_params(),
1019        "ask_user" => ask_user_params(),
1020        "pkg_manage" => pkg_manage_params(),
1021        "net_info" => net_info_params(),
1022        "net_scan" => net_scan_params(),
1023        "service_manage" => service_manage_params(),
1024        "user_manage" => user_manage_params(),
1025        "firewall" => firewall_params(),
1026        "ollama_manage" => ollama_manage_params(),
1027        "exo_manage" => exo_manage_params(),
1028        "uv_manage" => uv_manage_params(),
1029        "npm_manage" => npm_manage_params(),
1030        "agent_setup" => agent_setup_params(),
1031        _ => vec![],
1032    }
1033}
1034
1035/// OpenAI / OpenAI-compatible function-calling format.
1036///
1037/// ```json
1038/// { "type": "function", "function": { "name", "description", "parameters": { … } } }
1039/// ```
1040pub fn tools_openai() -> Vec<Value> {
1041    all_tools()
1042        .into_iter()
1043        .map(|t| {
1044            let params = resolve_params(t);
1045            let (properties, required) = params_to_json_schema(&params);
1046            json!({
1047                "type": "function",
1048                "function": {
1049                    "name": t.name,
1050                    "description": t.description,
1051                    "parameters": {
1052                        "type": "object",
1053                        "properties": properties,
1054                        "required": required,
1055                    }
1056                }
1057            })
1058        })
1059        .collect()
1060}
1061
1062/// Anthropic tool-use format.
1063///
1064/// ```json
1065/// { "name", "description", "input_schema": { … } }
1066/// ```
1067pub fn tools_anthropic() -> Vec<Value> {
1068    all_tools()
1069        .into_iter()
1070        .map(|t| {
1071            let params = resolve_params(t);
1072            let (properties, required) = params_to_json_schema(&params);
1073            json!({
1074                "name": t.name,
1075                "description": t.description,
1076                "input_schema": {
1077                    "type": "object",
1078                    "properties": properties,
1079                    "required": required,
1080                }
1081            })
1082        })
1083        .collect()
1084}
1085
1086/// Google Gemini function-declaration format.
1087///
1088/// ```json
1089/// { "name", "description", "parameters": { … } }
1090/// ```
1091pub fn tools_google() -> Vec<Value> {
1092    all_tools()
1093        .into_iter()
1094        .map(|t| {
1095            let params = resolve_params(t);
1096            let (properties, required) = params_to_json_schema(&params);
1097            json!({
1098                "name": t.name,
1099                "description": t.description,
1100                "parameters": {
1101                    "type": "object",
1102                    "properties": properties,
1103                    "required": required,
1104                }
1105            })
1106        })
1107        .collect()
1108}
1109
1110// ── Tool execution ──────────────────────────────────────────────────────────
1111
1112/// Returns `true` for tools that must be routed through the gateway
1113/// (i.e. handled by `execute_secrets_tool`) rather than `execute_tool`.
1114pub fn is_secrets_tool(name: &str) -> bool {
1115    matches!(name, "secrets_list" | "secrets_get" | "secrets_store")
1116}
1117
1118/// Returns `true` for skill-management tools that are routed through the
1119/// gateway (i.e. handled by `execute_skill_tool`) because they need access
1120/// to the process-global `SkillManager`.
1121pub fn is_skill_tool(name: &str) -> bool {
1122    matches!(
1123        name,
1124        "skill_list"
1125            | "skill_search"
1126            | "skill_install"
1127            | "skill_info"
1128            | "skill_enable"
1129            | "skill_link_secret"
1130            | "skill_create"
1131    )
1132}
1133
1134/// Returns `true` for the interactive prompt tool that must be routed
1135/// through the gateway → TUI → user → gateway → tool-result path.
1136pub fn is_user_prompt_tool(name: &str) -> bool {
1137    name == "ask_user"
1138}
1139
1140/// Find a tool by name and execute it with the given arguments.
1141#[instrument(skip(args, workspace_dir), fields(tool = name))]
1142pub fn execute_tool(name: &str, args: &Value, workspace_dir: &Path) -> Result<String, String> {
1143    debug!("Executing tool");
1144    for tool in all_tools() {
1145        if tool.name == name {
1146            let result = (tool.execute)(args, workspace_dir);
1147            if result.is_err() {
1148                warn!(error = ?result.as_ref().err(), "Tool execution failed");
1149            }
1150            return result;
1151        }
1152    }
1153    warn!(tool = name, "Unknown tool requested");
1154    Err(format!("Unknown tool: {}", name))
1155}
1156
1157// ── Wire types for WebSocket protocol ───────────────────────────────────────
1158
1159/// A tool call requested by the model (sent gateway → client for display).
1160#[derive(Debug, Clone, Serialize, Deserialize)]
1161pub struct ToolCall {
1162    pub id: String,
1163    pub name: String,
1164    pub arguments: Value,
1165}
1166
1167/// The result of executing a tool (sent gateway → client for display,
1168/// and also injected back into the conversation for the model).
1169#[derive(Debug, Clone, Serialize, Deserialize)]
1170pub struct ToolResult {
1171    pub id: String,
1172    pub name: String,
1173    pub result: String,
1174    pub is_error: bool,
1175}
1176
1177#[cfg(test)]
1178mod tests {
1179    use super::*;
1180    use std::path::Path;
1181
1182    /// Helper: return the project root as workspace dir for tests.
1183    fn ws() -> &'static Path {
1184        // In the workspace, CARGO_MANIFEST_DIR is crates/rustyclaw-core.
1185        // The workspace root is two levels up.
1186        Path::new(env!("CARGO_MANIFEST_DIR"))
1187            .parent()
1188            .unwrap()
1189            .parent()
1190            .unwrap()
1191    }
1192
1193    // ── read_file ───────────────────────────────────────────────────
1194
1195    #[test]
1196    fn test_read_file_this_file() {
1197        let args = json!({ "path": file!(), "start_line": 1, "end_line": 5 });
1198        let result = exec_read_file(&args, ws());
1199        assert!(result.is_ok());
1200        let text = result.unwrap();
1201        assert!(text.contains("Agent tool system"));
1202    }
1203
1204    #[test]
1205    fn test_read_file_missing() {
1206        let args = json!({ "path": "/nonexistent/file.txt" });
1207        let result = exec_read_file(&args, ws());
1208        assert!(result.is_err());
1209    }
1210
1211    #[test]
1212    fn test_read_file_no_path() {
1213        let args = json!({});
1214        let result = exec_read_file(&args, ws());
1215        assert!(result.is_err());
1216        assert!(result.unwrap_err().contains("Missing required parameter"));
1217    }
1218
1219    #[test]
1220    fn test_read_file_relative() {
1221        // Relative path should resolve against workspace_dir.
1222        let args = json!({ "path": "Cargo.toml", "start_line": 1, "end_line": 3 });
1223        let result = exec_read_file(&args, ws());
1224        assert!(result.is_ok());
1225        let text = result.unwrap();
1226        assert!(text.contains("workspace"));
1227    }
1228
1229    // ── write_file ──────────────────────────────────────────────────
1230
1231    #[test]
1232    fn test_write_file_and_read_back() {
1233        let dir = std::env::temp_dir().join("rustyclaw_test_write");
1234        let _ = std::fs::remove_dir_all(&dir);
1235        let args = json!({
1236            "path": "sub/test.txt",
1237            "content": "hello world"
1238        });
1239        let result = exec_write_file(&args, &dir);
1240        assert!(result.is_ok());
1241        assert!(result.unwrap().contains("11 bytes"));
1242
1243        let content = std::fs::read_to_string(dir.join("sub/test.txt")).unwrap();
1244        assert_eq!(content, "hello world");
1245        let _ = std::fs::remove_dir_all(&dir);
1246    }
1247
1248    // ── edit_file ───────────────────────────────────────────────────
1249
1250    #[test]
1251    fn test_edit_file_single_match() {
1252        let dir = std::env::temp_dir().join("rustyclaw_test_edit");
1253        let _ = std::fs::remove_dir_all(&dir);
1254        std::fs::create_dir_all(&dir).unwrap();
1255        std::fs::write(dir.join("f.txt"), "aaa\nbbb\nccc\n").unwrap();
1256
1257        let args = json!({ "path": "f.txt", "old_string": "bbb", "new_string": "BBB" });
1258        let result = exec_edit_file(&args, &dir);
1259        assert!(result.is_ok());
1260
1261        let content = std::fs::read_to_string(dir.join("f.txt")).unwrap();
1262        assert_eq!(content, "aaa\nBBB\nccc\n");
1263        let _ = std::fs::remove_dir_all(&dir);
1264    }
1265
1266    #[test]
1267    fn test_edit_file_no_match() {
1268        let dir = std::env::temp_dir().join("rustyclaw_test_edit_no");
1269        let _ = std::fs::remove_dir_all(&dir);
1270        std::fs::create_dir_all(&dir).unwrap();
1271        std::fs::write(dir.join("f.txt"), "aaa\nbbb\n").unwrap();
1272
1273        let args = json!({ "path": "f.txt", "old_string": "zzz", "new_string": "ZZZ" });
1274        let result = exec_edit_file(&args, &dir);
1275        assert!(result.is_err());
1276        assert!(result.unwrap_err().contains("not found"));
1277        let _ = std::fs::remove_dir_all(&dir);
1278    }
1279
1280    #[test]
1281    fn test_edit_file_multiple_matches() {
1282        let dir = std::env::temp_dir().join("rustyclaw_test_edit_multi");
1283        let _ = std::fs::remove_dir_all(&dir);
1284        std::fs::create_dir_all(&dir).unwrap();
1285        std::fs::write(dir.join("f.txt"), "aaa\naaa\n").unwrap();
1286
1287        let args = json!({ "path": "f.txt", "old_string": "aaa", "new_string": "bbb" });
1288        let result = exec_edit_file(&args, &dir);
1289        assert!(result.is_err());
1290        assert!(result.unwrap_err().contains("2 times"));
1291        let _ = std::fs::remove_dir_all(&dir);
1292    }
1293
1294    // ── list_directory ──────────────────────────────────────────────
1295
1296    #[test]
1297    fn test_list_directory() {
1298        let args = json!({ "path": "crates/rustyclaw-core/src" });
1299        let result = exec_list_directory(&args, ws());
1300        assert!(result.is_ok());
1301        let text = result.unwrap();
1302        // tools is now a directory
1303        assert!(text.contains("tools/"));
1304        assert!(text.contains("lib.rs"));
1305    }
1306
1307    // ── search_files ────────────────────────────────────────────────
1308
1309    #[test]
1310    fn test_search_files_finds_pattern() {
1311        let args = json!({ "pattern": "exec_read_file", "path": "crates/rustyclaw-core/src", "include": "*.rs" });
1312        let result = exec_search_files(&args, ws());
1313        assert!(result.is_ok());
1314        let text = result.unwrap();
1315        // The function is now in tools/file.rs
1316        assert!(text.contains("tools/file.rs") || text.contains("tools\\file.rs"));
1317    }
1318
1319    #[test]
1320    fn test_search_files_no_match() {
1321        let dir = std::env::temp_dir().join("rustyclaw_test_search_none");
1322        let _ = std::fs::remove_dir_all(&dir);
1323        std::fs::create_dir_all(&dir).unwrap();
1324        std::fs::write(dir.join("a.txt"), "hello world\n").unwrap();
1325
1326        let args = json!({ "pattern": "XYZZY_NEVER_42" });
1327        let result = exec_search_files(&args, &dir);
1328        assert!(result.is_ok());
1329        assert!(result.unwrap().contains("No matches"));
1330        let _ = std::fs::remove_dir_all(&dir);
1331    }
1332
1333    // ── find_files ──────────────────────────────────────────────────
1334
1335    #[test]
1336    fn test_find_files_glob() {
1337        let args = json!({ "pattern": "*.toml" });
1338        let result = exec_find_files(&args, ws());
1339        assert!(result.is_ok());
1340        let text = result.unwrap();
1341        assert!(text.contains("Cargo.toml"));
1342    }
1343
1344    #[test]
1345    fn test_find_files_keyword_case_insensitive() {
1346        // "cargo" should match "Cargo.toml" (case-insensitive).
1347        let args = json!({ "pattern": "cargo" });
1348        let result = exec_find_files(&args, ws());
1349        assert!(result.is_ok());
1350        let text = result.unwrap();
1351        assert!(text.contains("Cargo.toml"));
1352    }
1353
1354    #[test]
1355    fn test_find_files_multiple_keywords() {
1356        // Space-separated keywords: match ANY.
1357        let args = json!({ "pattern": "cargo license" });
1358        let result = exec_find_files(&args, ws());
1359        assert!(result.is_ok());
1360        let text = result.unwrap();
1361        assert!(text.contains("Cargo.toml"));
1362        assert!(text.contains("LICENSE"));
1363    }
1364
1365    #[test]
1366    fn test_find_files_keyword_no_match() {
1367        let dir = std::env::temp_dir().join("rustyclaw_test_find_kw");
1368        let _ = std::fs::remove_dir_all(&dir);
1369        std::fs::create_dir_all(&dir).unwrap();
1370        std::fs::write(dir.join("hello.txt"), "content").unwrap();
1371
1372        let args = json!({ "pattern": "resume" });
1373        let result = exec_find_files(&args, &dir);
1374        assert!(result.is_ok());
1375        assert!(result.unwrap().contains("No files found"));
1376        let _ = std::fs::remove_dir_all(&dir);
1377    }
1378
1379    // ── execute_command ─────────────────────────────────────────────
1380
1381    #[test]
1382    fn test_execute_command_echo() {
1383        let args = json!({ "command": "echo hello" });
1384        let result = exec_execute_command(&args, ws());
1385        assert!(result.is_ok());
1386        assert!(result.unwrap().contains("hello"));
1387    }
1388
1389    #[test]
1390    fn test_execute_command_failure() {
1391        let args = json!({ "command": "false" });
1392        let result = exec_execute_command(&args, ws());
1393        assert!(result.is_ok()); // still returns Ok with exit code
1394        assert!(result.unwrap().contains("exit code"));
1395    }
1396
1397    // ── execute_tool dispatch ───────────────────────────────────────
1398
1399    #[test]
1400    fn test_execute_tool_dispatch() {
1401        let args = json!({ "path": file!() });
1402        let result = execute_tool("read_file", &args, ws());
1403        assert!(result.is_ok());
1404    }
1405
1406    #[test]
1407    fn test_execute_tool_unknown() {
1408        let result = execute_tool("no_such_tool", &json!({}), ws());
1409        assert!(result.is_err());
1410    }
1411
1412    // ── Provider format tests ───────────────────────────────────────
1413
1414    #[test]
1415    fn test_openai_format() {
1416        let tools = tools_openai();
1417        assert_eq!(tools.len(), 61);
1418        assert_eq!(tools[0]["type"], "function");
1419        assert_eq!(tools[0]["function"]["name"], "read_file");
1420        assert!(tools[0]["function"]["parameters"]["properties"]["path"].is_object());
1421    }
1422
1423    #[test]
1424    fn test_anthropic_format() {
1425        let tools = tools_anthropic();
1426        assert_eq!(tools.len(), 61);
1427        assert_eq!(tools[0]["name"], "read_file");
1428        assert!(tools[0]["input_schema"]["properties"]["path"].is_object());
1429    }
1430
1431    #[test]
1432    fn test_google_format() {
1433        let tools = tools_google();
1434        assert_eq!(tools.len(), 61);
1435        assert_eq!(tools[0]["name"], "read_file");
1436    }
1437
1438    // ── resolve_path helper ─────────────────────────────────────────
1439
1440    #[test]
1441    fn test_resolve_path_absolute() {
1442        let result = helpers::resolve_path(Path::new("/workspace"), "/absolute/path.txt");
1443        assert_eq!(result, std::path::PathBuf::from("/absolute/path.txt"));
1444    }
1445
1446    #[test]
1447    fn test_resolve_path_relative() {
1448        let result = helpers::resolve_path(Path::new("/workspace"), "relative/path.txt");
1449        assert_eq!(result, std::path::PathBuf::from("/workspace/relative/path.txt"));
1450    }
1451
1452    // ── web_fetch ───────────────────────────────────────────────────
1453
1454    #[test]
1455    fn test_web_fetch_missing_url() {
1456        let args = json!({});
1457        let result = exec_web_fetch(&args, ws());
1458        assert!(result.is_err());
1459        assert!(result.unwrap_err().contains("Missing required parameter"));
1460    }
1461
1462    #[test]
1463    fn test_web_fetch_invalid_url() {
1464        let args = json!({ "url": "not-a-url" });
1465        let result = exec_web_fetch(&args, ws());
1466        assert!(result.is_err());
1467        assert!(result.unwrap_err().contains("http"));
1468    }
1469
1470    #[test]
1471    fn test_web_fetch_params_defined() {
1472        let params = web_fetch_params();
1473        assert_eq!(params.len(), 4);
1474        assert!(params.iter().any(|p| p.name == "url" && p.required));
1475        assert!(params.iter().any(|p| p.name == "extract_mode" && !p.required));
1476        assert!(params.iter().any(|p| p.name == "max_chars" && !p.required));
1477        assert!(params.iter().any(|p| p.name == "use_cookies" && !p.required));
1478    }
1479
1480    // ── web_search ──────────────────────────────────────────────────
1481
1482    #[test]
1483    fn test_web_search_missing_query() {
1484        let args = json!({});
1485        let result = exec_web_search(&args, ws());
1486        assert!(result.is_err());
1487        assert!(result.unwrap_err().contains("Missing required parameter"));
1488    }
1489
1490    #[test]
1491    fn test_web_search_no_api_key() {
1492        // Clear any existing key for the test
1493        // SAFETY: This test is single-threaded and no other thread reads BRAVE_API_KEY.
1494        unsafe { std::env::remove_var("BRAVE_API_KEY") };
1495        let args = json!({ "query": "test" });
1496        let result = exec_web_search(&args, ws());
1497        assert!(result.is_err());
1498        assert!(result.unwrap_err().contains("BRAVE_API_KEY"));
1499    }
1500
1501    #[test]
1502    fn test_web_search_params_defined() {
1503        let params = web_search_params();
1504        assert_eq!(params.len(), 5);
1505        assert!(params.iter().any(|p| p.name == "query" && p.required));
1506        assert!(params.iter().any(|p| p.name == "count" && !p.required));
1507        assert!(params.iter().any(|p| p.name == "country" && !p.required));
1508        assert!(params.iter().any(|p| p.name == "search_lang" && !p.required));
1509        assert!(params.iter().any(|p| p.name == "freshness" && !p.required));
1510    }
1511
1512    // ── process ─────────────────────────────────────────────────────
1513
1514    #[test]
1515    fn test_process_missing_action() {
1516        let args = json!({});
1517        let result = exec_process(&args, ws());
1518        assert!(result.is_err());
1519        assert!(result.unwrap_err().contains("Missing required parameter"));
1520    }
1521
1522    #[test]
1523    fn test_process_invalid_action() {
1524        let args = json!({ "action": "invalid" });
1525        let result = exec_process(&args, ws());
1526        assert!(result.is_err());
1527        assert!(result.unwrap_err().contains("Unknown action"));
1528    }
1529
1530    #[test]
1531    fn test_process_list_empty() {
1532        let args = json!({ "action": "list" });
1533        let result = exec_process(&args, ws());
1534        assert!(result.is_ok());
1535        // May have sessions from other tests, so just check it doesn't error
1536    }
1537
1538    #[test]
1539    fn test_process_params_defined() {
1540        let params = process_params();
1541        assert_eq!(params.len(), 6);
1542        assert!(params.iter().any(|p| p.name == "action" && p.required));
1543        assert!(params.iter().any(|p| p.name == "sessionId" && !p.required));
1544        assert!(params.iter().any(|p| p.name == "data" && !p.required));
1545        assert!(params.iter().any(|p| p.name == "keys" && !p.required));
1546        assert!(params.iter().any(|p| p.name == "offset" && !p.required));
1547        assert!(params.iter().any(|p| p.name == "limit" && !p.required));
1548    }
1549
1550    #[test]
1551    fn test_execute_command_params_with_background() {
1552        let params = execute_command_params();
1553        assert_eq!(params.len(), 5);
1554        assert!(params.iter().any(|p| p.name == "command" && p.required));
1555        assert!(params.iter().any(|p| p.name == "background" && !p.required));
1556        assert!(params.iter().any(|p| p.name == "yieldMs" && !p.required));
1557    }
1558
1559    // ── memory_search ───────────────────────────────────────────────
1560
1561    #[test]
1562    fn test_memory_search_params_defined() {
1563        let params = memory_search_params();
1564        assert_eq!(params.len(), 5);
1565        assert!(params.iter().any(|p| p.name == "query" && p.required));
1566        assert!(params.iter().any(|p| p.name == "maxResults" && !p.required));
1567        assert!(params.iter().any(|p| p.name == "minScore" && !p.required));
1568        assert!(params.iter().any(|p| p.name == "recencyBoost" && !p.required));
1569        assert!(params.iter().any(|p| p.name == "halfLifeDays" && !p.required));
1570    }
1571
1572    #[test]
1573    fn test_memory_search_missing_query() {
1574        let args = json!({});
1575        let result = exec_memory_search(&args, ws());
1576        assert!(result.is_err());
1577        assert!(result.unwrap_err().contains("Missing required parameter"));
1578    }
1579
1580    // ── memory_get ──────────────────────────────────────────────────
1581
1582    #[test]
1583    fn test_memory_get_params_defined() {
1584        let params = memory_get_params();
1585        assert_eq!(params.len(), 3);
1586        assert!(params.iter().any(|p| p.name == "path" && p.required));
1587        assert!(params.iter().any(|p| p.name == "from" && !p.required));
1588        assert!(params.iter().any(|p| p.name == "lines" && !p.required));
1589    }
1590
1591    #[test]
1592    fn test_memory_get_missing_path() {
1593        let args = json!({});
1594        let result = exec_memory_get(&args, ws());
1595        assert!(result.is_err());
1596        assert!(result.unwrap_err().contains("Missing required parameter"));
1597    }
1598
1599    #[test]
1600    fn test_memory_get_invalid_path() {
1601        let args = json!({ "path": "../etc/passwd" });
1602        let result = exec_memory_get(&args, ws());
1603        assert!(result.is_err());
1604        assert!(result.unwrap_err().contains("not a valid memory file"));
1605    }
1606
1607    // ── cron ────────────────────────────────────────────────────────
1608
1609    #[test]
1610    fn test_cron_params_defined() {
1611        let params = cron_params();
1612        assert_eq!(params.len(), 5);
1613        assert!(params.iter().any(|p| p.name == "action" && p.required));
1614        assert!(params.iter().any(|p| p.name == "jobId" && !p.required));
1615    }
1616
1617    #[test]
1618    fn test_cron_missing_action() {
1619        let args = json!({});
1620        let result = exec_cron(&args, ws());
1621        assert!(result.is_err());
1622        assert!(result.unwrap_err().contains("Missing required parameter"));
1623    }
1624
1625    #[test]
1626    fn test_cron_invalid_action() {
1627        let args = json!({ "action": "invalid" });
1628        let result = exec_cron(&args, ws());
1629        assert!(result.is_err());
1630        assert!(result.unwrap_err().contains("Unknown action"));
1631    }
1632
1633    // ── sessions_list ───────────────────────────────────────────────
1634
1635    #[test]
1636    fn test_sessions_list_params_defined() {
1637        let params = sessions_list_params();
1638        assert_eq!(params.len(), 4);
1639        assert!(params.iter().all(|p| !p.required));
1640    }
1641
1642    // ── sessions_spawn ──────────────────────────────────────────────
1643
1644    #[test]
1645    fn test_sessions_spawn_params_defined() {
1646        let params = sessions_spawn_params();
1647        assert_eq!(params.len(), 7);
1648        assert!(params.iter().any(|p| p.name == "task" && p.required));
1649    }
1650
1651    #[test]
1652    fn test_sessions_spawn_missing_task() {
1653        let args = json!({});
1654        let result = exec_sessions_spawn(&args, ws());
1655        assert!(result.is_err());
1656        assert!(result.unwrap_err().contains("Missing required parameter"));
1657    }
1658
1659    // ── sessions_send ───────────────────────────────────────────────
1660
1661    #[test]
1662    fn test_sessions_send_params_defined() {
1663        let params = sessions_send_params();
1664        assert_eq!(params.len(), 4);
1665        assert!(params.iter().any(|p| p.name == "message" && p.required));
1666    }
1667
1668    #[test]
1669    fn test_sessions_send_missing_message() {
1670        let args = json!({});
1671        let result = exec_sessions_send(&args, ws());
1672        assert!(result.is_err());
1673        assert!(result.unwrap_err().contains("Missing required parameter"));
1674    }
1675
1676    // ── sessions_history ────────────────────────────────────────────
1677
1678    #[test]
1679    fn test_sessions_history_params_defined() {
1680        let params = sessions_history_params();
1681        assert_eq!(params.len(), 3);
1682        assert!(params.iter().any(|p| p.name == "sessionKey" && p.required));
1683    }
1684
1685    // ── session_status ──────────────────────────────────────────────
1686
1687    #[test]
1688    fn test_session_status_params_defined() {
1689        let params = session_status_params();
1690        assert_eq!(params.len(), 2);
1691        assert!(params.iter().all(|p| !p.required));
1692    }
1693
1694    #[test]
1695    fn test_session_status_general() {
1696        let args = json!({});
1697        let result = exec_session_status(&args, ws());
1698        assert!(result.is_ok());
1699        assert!(result.unwrap().contains("Session Status"));
1700    }
1701
1702    // ── agents_list ─────────────────────────────────────────────────
1703
1704    #[test]
1705    fn test_agents_list_params_defined() {
1706        let params = agents_list_params();
1707        assert_eq!(params.len(), 0);
1708    }
1709
1710    #[test]
1711    fn test_agents_list_returns_main() {
1712        let args = json!({});
1713        let result = exec_agents_list(&args, ws());
1714        assert!(result.is_ok());
1715        assert!(result.unwrap().contains("main"));
1716    }
1717
1718    // ── apply_patch ─────────────────────────────────────────────────
1719
1720    #[test]
1721    fn test_apply_patch_params_defined() {
1722        let params = apply_patch_params();
1723        assert_eq!(params.len(), 3);
1724        assert!(params.iter().any(|p| p.name == "patch" && p.required));
1725        assert!(params.iter().any(|p| p.name == "dry_run" && !p.required));
1726    }
1727
1728    #[test]
1729    fn test_apply_patch_missing_patch() {
1730        let args = json!({});
1731        let result = exec_apply_patch(&args, ws());
1732        assert!(result.is_err());
1733        assert!(result.unwrap_err().contains("Missing required parameter"));
1734    }
1735
1736    #[test]
1737    fn test_parse_unified_diff() {
1738        let patch_str = r#"--- a/test.txt
1739+++ b/test.txt
1740@@ -1,3 +1,4 @@
1741 line1
1742+new line
1743 line2
1744 line3
1745"#;
1746        let hunks = patch::parse_unified_diff(patch_str).unwrap();
1747        assert_eq!(hunks.len(), 1);
1748        assert_eq!(hunks[0].file_path, "test.txt");
1749        assert_eq!(hunks[0].old_start, 1);
1750        assert_eq!(hunks[0].old_count, 3);
1751    }
1752
1753    // ── secrets tools ───────────────────────────────────────────────
1754
1755    #[test]
1756    fn test_secrets_stub_rejects() {
1757        let args = json!({});
1758        let result = exec_secrets_stub(&args, ws());
1759        assert!(result.is_err());
1760        assert!(result.unwrap_err().contains("gateway"));
1761    }
1762
1763    #[test]
1764    fn test_is_secrets_tool() {
1765        assert!(is_secrets_tool("secrets_list"));
1766        assert!(is_secrets_tool("secrets_get"));
1767        assert!(is_secrets_tool("secrets_store"));
1768        assert!(!is_secrets_tool("read_file"));
1769        assert!(!is_secrets_tool("memory_get"));
1770    }
1771
1772    #[test]
1773    fn test_secrets_list_params_defined() {
1774        let params = secrets_list_params();
1775        assert_eq!(params.len(), 1);
1776        assert!(params.iter().any(|p| p.name == "prefix" && !p.required));
1777    }
1778
1779    #[test]
1780    fn test_secrets_get_params_defined() {
1781        let params = secrets_get_params();
1782        assert_eq!(params.len(), 1);
1783        assert!(params.iter().any(|p| p.name == "key" && p.required));
1784    }
1785
1786    #[test]
1787    fn test_secrets_store_params_defined() {
1788        let params = secrets_store_params();
1789        assert_eq!(params.len(), 2);
1790        assert!(params.iter().any(|p| p.name == "key" && p.required));
1791        assert!(params.iter().any(|p| p.name == "value" && p.required));
1792    }
1793
1794    #[test]
1795    fn test_protected_path_without_init() {
1796        // Before set_credentials_dir is called, nothing is protected.
1797        assert!(!is_protected_path(Path::new("/some/random/path")));
1798    }
1799
1800    // ── gateway ─────────────────────────────────────────────────────
1801
1802    #[test]
1803    fn test_gateway_params_defined() {
1804        let params = gateway_params();
1805        assert_eq!(params.len(), 5);
1806        assert!(params.iter().any(|p| p.name == "action" && p.required));
1807    }
1808
1809    #[test]
1810    fn test_gateway_missing_action() {
1811        let args = json!({});
1812        let result = exec_gateway(&args, ws());
1813        assert!(result.is_err());
1814        assert!(result.unwrap_err().contains("Missing required parameter"));
1815    }
1816
1817    #[test]
1818    fn test_gateway_config_schema() {
1819        let args = json!({ "action": "config.schema" });
1820        let result = exec_gateway(&args, ws());
1821        assert!(result.is_ok());
1822        assert!(result.unwrap().contains("properties"));
1823    }
1824
1825    // ── message ─────────────────────────────────────────────────────
1826
1827    #[test]
1828    fn test_message_params_defined() {
1829        let params = message_params();
1830        assert_eq!(params.len(), 7);
1831        assert!(params.iter().any(|p| p.name == "action" && p.required));
1832    }
1833
1834    #[test]
1835    fn test_message_missing_action() {
1836        let args = json!({});
1837        let result = exec_message(&args, ws());
1838        assert!(result.is_err());
1839        assert!(result.unwrap_err().contains("Missing required parameter"));
1840    }
1841
1842    // ── tts ─────────────────────────────────────────────────────────
1843
1844    #[test]
1845    fn test_tts_params_defined() {
1846        let params = tts_params();
1847        assert_eq!(params.len(), 2);
1848        assert!(params.iter().any(|p| p.name == "text" && p.required));
1849    }
1850
1851    #[test]
1852    fn test_tts_missing_text() {
1853        let args = json!({});
1854        let result = exec_tts(&args, ws());
1855        assert!(result.is_err());
1856        assert!(result.unwrap_err().contains("Missing required parameter"));
1857    }
1858
1859    #[test]
1860    fn test_tts_returns_media_path() {
1861        let args = json!({ "text": "Hello world" });
1862        let result = exec_tts(&args, ws());
1863        assert!(result.is_ok());
1864        assert!(result.unwrap().contains("MEDIA:"));
1865    }
1866
1867    // ── image ───────────────────────────────────────────────────────
1868
1869    #[test]
1870    fn test_image_params_defined() {
1871        let params = image_params();
1872        assert_eq!(params.len(), 2);
1873        assert!(params.iter().any(|p| p.name == "image" && p.required));
1874        assert!(params.iter().any(|p| p.name == "prompt" && !p.required));
1875    }
1876
1877    #[test]
1878    fn test_image_missing_image() {
1879        let args = json!({});
1880        let result = exec_image(&args, ws());
1881        assert!(result.is_err());
1882        assert!(result.unwrap_err().contains("Missing required parameter"));
1883    }
1884
1885    #[test]
1886    fn test_image_url_detection() {
1887        let args = json!({ "image": "https://example.com/photo.jpg" });
1888        let result = exec_image(&args, ws());
1889        assert!(result.is_ok());
1890        assert!(result.unwrap().contains("Is URL: true"));
1891    }
1892
1893    // ── nodes ───────────────────────────────────────────────────────
1894
1895    #[test]
1896    fn test_nodes_params_defined() {
1897        let params = nodes_params();
1898        assert_eq!(params.len(), 8);
1899        assert!(params.iter().any(|p| p.name == "action" && p.required));
1900        assert!(params.iter().any(|p| p.name == "node" && !p.required));
1901    }
1902
1903    #[test]
1904    fn test_nodes_missing_action() {
1905        let args = json!({});
1906        let result = exec_nodes(&args, ws());
1907        assert!(result.is_err());
1908        assert!(result.unwrap_err().contains("Missing required parameter"));
1909    }
1910
1911    #[test]
1912    fn test_nodes_status() {
1913        let args = json!({ "action": "status" });
1914        let result = exec_nodes(&args, ws());
1915        assert!(result.is_ok());
1916        let output = result.unwrap();
1917        assert!(output.contains("nodes"));
1918        assert!(output.contains("tools"));
1919    }
1920
1921    // ── browser ─────────────────────────────────────────────────────
1922
1923    #[test]
1924    fn test_browser_params_defined() {
1925        let params = browser_params();
1926        assert_eq!(params.len(), 7);
1927        assert!(params.iter().any(|p| p.name == "action" && p.required));
1928    }
1929
1930    #[test]
1931    fn test_browser_missing_action() {
1932        let args = json!({});
1933        let result = exec_browser(&args, ws());
1934        assert!(result.is_err());
1935        assert!(result.unwrap_err().contains("Missing required parameter"));
1936    }
1937
1938    #[test]
1939    fn test_browser_status() {
1940        let args = json!({ "action": "status" });
1941        let result = exec_browser(&args, ws());
1942        assert!(result.is_ok());
1943        let output = result.unwrap();
1944        assert!(output.contains("running"));
1945    }
1946
1947    // ── canvas ──────────────────────────────────────────────────────
1948
1949    #[test]
1950    fn test_canvas_params_defined() {
1951        let params = canvas_params();
1952        assert_eq!(params.len(), 6);
1953        assert!(params.iter().any(|p| p.name == "action" && p.required));
1954    }
1955
1956    #[test]
1957    fn test_canvas_missing_action() {
1958        let args = json!({});
1959        let result = exec_canvas(&args, ws());
1960        assert!(result.is_err());
1961        assert!(result.unwrap_err().contains("Missing required parameter"));
1962    }
1963
1964    #[test]
1965    fn test_canvas_snapshot() {
1966        let args = json!({ "action": "snapshot" });
1967        let result = exec_canvas(&args, ws());
1968        assert!(result.is_ok());
1969        let output = result.unwrap();
1970        // Without a URL presented first, snapshot returns no_canvas
1971        assert!(output.contains("no_canvas") || output.contains("snapshot_captured"));
1972    }
1973
1974    // ── skill tools ─────────────────────────────────────────────────
1975
1976    #[test]
1977    fn test_skill_list_params_defined() {
1978        let params = skill_list_params();
1979        assert_eq!(params.len(), 1);
1980        assert!(params.iter().any(|p| p.name == "filter" && !p.required));
1981    }
1982
1983    #[test]
1984    fn test_skill_search_params_defined() {
1985        let params = skill_search_params();
1986        assert_eq!(params.len(), 1);
1987        assert!(params.iter().any(|p| p.name == "query" && p.required));
1988    }
1989
1990    #[test]
1991    fn test_skill_install_params_defined() {
1992        let params = skill_install_params();
1993        assert_eq!(params.len(), 2);
1994        assert!(params.iter().any(|p| p.name == "name" && p.required));
1995        assert!(params.iter().any(|p| p.name == "version" && !p.required));
1996    }
1997
1998    #[test]
1999    fn test_skill_info_params_defined() {
2000        let params = skill_info_params();
2001        assert_eq!(params.len(), 1);
2002        assert!(params.iter().any(|p| p.name == "name" && p.required));
2003    }
2004
2005    #[test]
2006    fn test_skill_enable_params_defined() {
2007        let params = skill_enable_params();
2008        assert_eq!(params.len(), 2);
2009        assert!(params.iter().any(|p| p.name == "name" && p.required));
2010        assert!(params.iter().any(|p| p.name == "enabled" && p.required));
2011    }
2012
2013    #[test]
2014    fn test_skill_link_secret_params_defined() {
2015        let params = skill_link_secret_params();
2016        assert_eq!(params.len(), 3);
2017        assert!(params.iter().any(|p| p.name == "action" && p.required));
2018        assert!(params.iter().any(|p| p.name == "skill" && p.required));
2019        assert!(params.iter().any(|p| p.name == "secret" && p.required));
2020    }
2021
2022    #[test]
2023    fn test_skill_list_standalone_stub() {
2024        let result = exec_skill_list(&json!({}), ws());
2025        assert!(result.is_ok());
2026        assert!(result.unwrap().contains("standalone mode"));
2027    }
2028
2029    #[test]
2030    fn test_skill_search_missing_query() {
2031        let result = exec_skill_search(&json!({}), ws());
2032        assert!(result.is_err());
2033        assert!(result.unwrap_err().contains("Missing required parameter"));
2034    }
2035
2036    #[test]
2037    fn test_skill_install_missing_name() {
2038        let result = exec_skill_install(&json!({}), ws());
2039        assert!(result.is_err());
2040        assert!(result.unwrap_err().contains("Missing required parameter"));
2041    }
2042
2043    #[test]
2044    fn test_skill_info_missing_name() {
2045        let result = exec_skill_info(&json!({}), ws());
2046        assert!(result.is_err());
2047        assert!(result.unwrap_err().contains("Missing required parameter"));
2048    }
2049
2050    #[test]
2051    fn test_skill_enable_missing_params() {
2052        let result = exec_skill_enable(&json!({}), ws());
2053        assert!(result.is_err());
2054    }
2055
2056    #[test]
2057    fn test_skill_link_secret_bad_action() {
2058        let args = json!({ "action": "nope", "skill": "x", "secret": "y" });
2059        let result = exec_skill_link_secret(&args, ws());
2060        assert!(result.is_err());
2061        assert!(result.unwrap_err().contains("Unknown action"));
2062    }
2063
2064    #[test]
2065    fn test_is_skill_tool() {
2066        assert!(is_skill_tool("skill_list"));
2067        assert!(is_skill_tool("skill_search"));
2068        assert!(is_skill_tool("skill_install"));
2069        assert!(is_skill_tool("skill_info"));
2070        assert!(is_skill_tool("skill_enable"));
2071        assert!(is_skill_tool("skill_link_secret"));
2072        assert!(!is_skill_tool("read_file"));
2073        assert!(!is_skill_tool("secrets_list"));
2074    }
2075
2076    // ── disk_usage ──────────────────────────────────────────────────
2077
2078    #[test]
2079    fn test_disk_usage_params_defined() {
2080        let params = disk_usage_params();
2081        assert_eq!(params.len(), 3);
2082        assert!(params.iter().all(|p| !p.required));
2083    }
2084
2085    #[test]
2086    fn test_disk_usage_workspace() {
2087        let args = json!({ "path": ".", "depth": 1, "top": 5 });
2088        let result = exec_disk_usage(&args, ws());
2089        assert!(result.is_ok());
2090        assert!(result.unwrap().contains("entries"));
2091    }
2092
2093    #[test]
2094    fn test_disk_usage_nonexistent() {
2095        let args = json!({ "path": "/nonexistent_path_xyz" });
2096        let result = exec_disk_usage(&args, ws());
2097        assert!(result.is_err());
2098    }
2099
2100    // ── classify_files ──────────────────────────────────────────────
2101
2102    #[test]
2103    fn test_classify_files_params_defined() {
2104        let params = classify_files_params();
2105        assert_eq!(params.len(), 1);
2106        assert!(params[0].required);
2107    }
2108
2109    #[test]
2110    fn test_classify_files_workspace() {
2111        let args = json!({ "path": "." });
2112        let result = exec_classify_files(&args, ws());
2113        assert!(result.is_ok());
2114        let text = result.unwrap();
2115        assert!(text.contains("path"));
2116    }
2117
2118    #[test]
2119    fn test_classify_files_missing_path() {
2120        let args = json!({});
2121        let result = exec_classify_files(&args, ws());
2122        assert!(result.is_err());
2123        assert!(result.unwrap_err().contains("Missing required parameter"));
2124    }
2125
2126    // ── system_monitor ──────────────────────────────────────────────
2127
2128    #[test]
2129    fn test_system_monitor_params_defined() {
2130        let params = system_monitor_params();
2131        assert_eq!(params.len(), 1);
2132        assert!(!params[0].required);
2133    }
2134
2135    #[test]
2136    fn test_system_monitor_all() {
2137        let args = json!({});
2138        let result = exec_system_monitor(&args, ws());
2139        assert!(result.is_ok());
2140    }
2141
2142    #[test]
2143    fn test_system_monitor_cpu() {
2144        let args = json!({ "metric": "cpu" });
2145        let result = exec_system_monitor(&args, ws());
2146        assert!(result.is_ok());
2147    }
2148
2149    // ── battery_health ──────────────────────────────────────────────
2150
2151    #[test]
2152    fn test_battery_health_params_defined() {
2153        let params = battery_health_params();
2154        assert_eq!(params.len(), 0);
2155    }
2156
2157    #[test]
2158    fn test_battery_health_runs() {
2159        let args = json!({});
2160        let result = exec_battery_health(&args, ws());
2161        assert!(result.is_ok());
2162    }
2163
2164    // ── app_index ───────────────────────────────────────────────────
2165
2166    #[test]
2167    fn test_app_index_params_defined() {
2168        let params = app_index_params();
2169        assert_eq!(params.len(), 2);
2170        assert!(params.iter().all(|p| !p.required));
2171    }
2172
2173    #[test]
2174    fn test_app_index_runs() {
2175        let args = json!({ "filter": "nonexistent_app_xyz" });
2176        let result = exec_app_index(&args, ws());
2177        assert!(result.is_ok());
2178        assert!(result.unwrap().contains("apps"));
2179    }
2180
2181    // ── cloud_browse ────────────────────────────────────────────────
2182
2183    #[test]
2184    fn test_cloud_browse_params_defined() {
2185        let params = cloud_browse_params();
2186        assert_eq!(params.len(), 2);
2187        assert!(params.iter().all(|p| !p.required));
2188    }
2189
2190    #[test]
2191    fn test_cloud_browse_detect() {
2192        let args = json!({ "action": "detect" });
2193        let result = exec_cloud_browse(&args, ws());
2194        assert!(result.is_ok());
2195        assert!(result.unwrap().contains("cloud_folders"));
2196    }
2197
2198    #[test]
2199    fn test_cloud_browse_invalid_action() {
2200        let args = json!({ "action": "invalid" });
2201        let result = exec_cloud_browse(&args, ws());
2202        assert!(result.is_err());
2203    }
2204
2205    // ── browser_cache ───────────────────────────────────────────────
2206
2207    #[test]
2208    fn test_browser_cache_params_defined() {
2209        let params = browser_cache_params();
2210        assert_eq!(params.len(), 2);
2211        assert!(params.iter().all(|p| !p.required));
2212    }
2213
2214    #[test]
2215    fn test_browser_cache_scan() {
2216        let args = json!({ "action": "scan" });
2217        let result = exec_browser_cache(&args, ws());
2218        assert!(result.is_ok());
2219        assert!(result.unwrap().contains("caches"));
2220    }
2221
2222    // ── screenshot ──────────────────────────────────────────────────
2223
2224    #[test]
2225    fn test_screenshot_params_defined() {
2226        let params = screenshot_params();
2227        assert_eq!(params.len(), 3);
2228        assert!(params.iter().all(|p| !p.required));
2229    }
2230
2231    // ── clipboard ───────────────────────────────────────────────────
2232
2233    #[test]
2234    fn test_clipboard_params_defined() {
2235        let params = clipboard_params();
2236        assert_eq!(params.len(), 2);
2237        assert!(params.iter().any(|p| p.name == "action" && p.required));
2238    }
2239
2240    #[test]
2241    fn test_clipboard_missing_action() {
2242        let args = json!({});
2243        let result = exec_clipboard(&args, ws());
2244        assert!(result.is_err());
2245        assert!(result.unwrap_err().contains("Missing required parameter"));
2246    }
2247
2248    #[test]
2249    fn test_clipboard_invalid_action() {
2250        let args = json!({ "action": "invalid" });
2251        let result = exec_clipboard(&args, ws());
2252        assert!(result.is_err());
2253    }
2254
2255    // ── audit_sensitive ─────────────────────────────────────────────
2256
2257    #[test]
2258    fn test_audit_sensitive_params_defined() {
2259        let params = audit_sensitive_params();
2260        assert_eq!(params.len(), 2);
2261        assert!(params.iter().all(|p| !p.required));
2262    }
2263
2264    #[test]
2265    fn test_audit_sensitive_runs() {
2266        let dir = std::env::temp_dir().join("rustyclaw_test_audit");
2267        let _ = std::fs::remove_dir_all(&dir);
2268        std::fs::create_dir_all(&dir).unwrap();
2269        std::fs::write(dir.join("safe.txt"), "nothing sensitive here").unwrap();
2270        let args = json!({ "path": ".", "max_files": 10 });
2271        let result = exec_audit_sensitive(&args, &dir);
2272        assert!(result.is_ok());
2273        assert!(result.unwrap().contains("scanned_files"));
2274        let _ = std::fs::remove_dir_all(&dir);
2275    }
2276
2277    // ── secure_delete ───────────────────────────────────────────────
2278
2279    #[test]
2280    fn test_secure_delete_params_defined() {
2281        let params = secure_delete_params();
2282        assert_eq!(params.len(), 3);
2283        assert!(params.iter().any(|p| p.name == "path" && p.required));
2284    }
2285
2286    #[test]
2287    fn test_secure_delete_missing_path() {
2288        let args = json!({});
2289        let result = exec_secure_delete(&args, ws());
2290        assert!(result.is_err());
2291        assert!(result.unwrap_err().contains("Missing required parameter"));
2292    }
2293
2294    #[test]
2295    fn test_secure_delete_nonexistent() {
2296        let args = json!({ "path": "/tmp/nonexistent_rustyclaw_xyz" });
2297        let result = exec_secure_delete(&args, ws());
2298        assert!(result.is_err());
2299    }
2300
2301    #[test]
2302    fn test_secure_delete_requires_confirm() {
2303        let dir = std::env::temp_dir().join("rustyclaw_test_secdelete");
2304        let _ = std::fs::remove_dir_all(&dir);
2305        std::fs::create_dir_all(&dir).unwrap();
2306        std::fs::write(dir.join("victim.txt"), "data").unwrap();
2307        let args = json!({ "path": dir.join("victim.txt").display().to_string() });
2308        let result = exec_secure_delete(&args, ws());
2309        assert!(result.is_ok());
2310        assert!(result.unwrap().contains("confirm_required"));
2311        let _ = std::fs::remove_dir_all(&dir);
2312    }
2313
2314    #[test]
2315    fn test_secure_delete_with_confirm() {
2316        let dir = std::env::temp_dir().join("rustyclaw_test_secdelete2");
2317        let _ = std::fs::remove_dir_all(&dir);
2318        std::fs::create_dir_all(&dir).unwrap();
2319        let victim = dir.join("victim.txt");
2320        std::fs::write(&victim, "secret data").unwrap();
2321        let args = json!({
2322            "path": victim.display().to_string(),
2323            "confirm": true,
2324        });
2325        let result = exec_secure_delete(&args, ws());
2326        assert!(result.is_ok());
2327        assert!(result.unwrap().contains("deleted"));
2328        assert!(!victim.exists());
2329        let _ = std::fs::remove_dir_all(&dir);
2330    }
2331
2332    // ── summarize_file ──────────────────────────────────────────────
2333
2334    #[test]
2335    fn test_summarize_file_params_defined() {
2336        let params = summarize_file_params();
2337        assert_eq!(params.len(), 2);
2338        assert!(params.iter().any(|p| p.name == "path" && p.required));
2339    }
2340
2341    #[test]
2342    fn test_summarize_file_missing_path() {
2343        let args = json!({});
2344        let result = exec_summarize_file(&args, ws());
2345        assert!(result.is_err());
2346        assert!(result.unwrap_err().contains("Missing required parameter"));
2347    }
2348
2349    #[test]
2350    fn test_summarize_file_this_file() {
2351        let args = json!({ "path": file!(), "max_lines": 10 });
2352        let result = exec_summarize_file(&args, ws());
2353        assert!(result.is_ok());
2354        let text = result.unwrap();
2355        assert!(text.contains("text"));
2356        assert!(text.contains("total_lines"));
2357    }
2358
2359    #[test]
2360    fn test_summarize_file_nonexistent() {
2361        let args = json!({ "path": "/nonexistent/file.txt" });
2362        let result = exec_summarize_file(&args, ws());
2363        assert!(result.is_err());
2364    }
2365}