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