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