--- description: Scaffold a new tool (WASM or built-in Rust) with all boilerplate wired up allowed-tools: Read, Edit, Write, Glob, Grep, Bash(cargo fmt:*), Bash(cargo clippy:*), Bash(cargo test:*), Bash(cargo component:*), Bash(ls:*), Bash(mkdir:*) argument-hint: [description] model: opus --- Scaffold a new tool called `$ARGUMENTS` for the OptimClaw agent. First, determine the tool type and then follow the appropriate path. ## Step 0: Determine tool type Ask the user which type of tool to create: - **WASM tool** (recommended) - Sandboxed, dynamically loadable, external API integrations. Lives in `tools-src//`. This is the right choice for anything that talks to an external service (Notion, GitHub, Discord, etc.). - **Built-in tool** - Compiled into the main binary. Only for core agent infrastructure (e.g., memory, file ops, shell). Lives in `src/tools/builtin/.rs`. If the description clearly implies an external service integration, default to WASM. If it's a core agent capability, default to built-in. --- ## Path A: WASM Tool ### A1: Create directory structure Create `tools-src//` with: ``` tools-src// ├── Cargo.toml ├── -tool.capabilities.json └── src/ ├── lib.rs ├── types.rs └── api.rs ``` ### A2: Write `Cargo.toml` Follow this exact pattern (adjust name and description): ```toml [package] name = "-tool" version = "0.1.0" edition = "2021" description = " tool for OptimClaw (WASM component)" license = "MIT OR Apache-2.0" publish = false [lib] crate-type = ["cdylib"] [dependencies] wit-bindgen = "=0.36" serde = { version = "1", features = ["derive"] } serde_json = "1" [profile.release] opt-level = "s" lto = true strip = true codegen-units = 1 ``` ### A3: Write `-tool.capabilities.json` Declare the tool's security requirements. Determine what APIs it needs and create the allowlist. Reference `tools-src/slack/slack-tool.capabilities.json` for the format. Key sections to include: - `http.allowlist` - API endpoints (host, path_prefix, methods) - `http.credentials` - Secret injection config (secret_name, location type: bearer/header/query) - `http.rate_limit` - requests_per_minute, requests_per_hour - `http.timeout_secs` - `secrets.allowed_names` - Which secrets the tool can check existence of - `auth` - Authentication setup (OAuth or manual token entry) If the tool needs OAuth, include: ```json { "auth": { "secret_name": "_token", "display_name": "", "oauth": { "authorization_url": "https://...", "token_url": "https://...", "client_id_env": "_OAUTH_CLIENT_ID", "client_secret_env": "_OAUTH_CLIENT_SECRET", "scopes": [], "use_pkce": false }, "env_var": "_TOKEN" } } ``` If no OAuth, include manual setup instructions: ```json { "auth": { "secret_name": "_api_key", "display_name": "", "instructions": "Get your API key from ", "setup_url": "https://...", "token_hint": "Starts with ''", "env_var": "_API_KEY" } } ``` ### A4: Write `src/types.rs` Define the action enum using serde's tagged enum pattern: ```rust use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize)] #[serde(tag = "action", rename_all = "snake_case")] pub enum Action { // Add variants based on the tool's capabilities. // Each variant maps to one API operation. } ``` Add result structs with `#[derive(Debug, Serialize)]`. Use `#[serde(skip_serializing_if = "Option::is_none")]` for optional fields. ### A5: Write `src/api.rs` Implement the API calls using the host HTTP capability: ```rust use crate::near::agent::host; use crate::types::*; const API_BASE: &str = "https://api.example.com"; fn api_call(method: &str, endpoint: &str, body: Option<&str>) -> Result { let url = format!("{}/{}", API_BASE, endpoint); let headers = if body.is_some() { r#"{"Content-Type": "application/json"}"# } else { "{}" }; let body_bytes = body.map(|b| b.as_bytes().to_vec()); host::log(host::LogLevel::Debug, &format!("API: {} {}", method, endpoint)); let response = host::http_request(method, &url, headers, body_bytes.as_deref())?; if response.status < 200 || response.status >= 300 { return Err(format!( "API returned status {}: {}", response.status, String::from_utf8_lossy(&response.body) )); } String::from_utf8(response.body).map_err(|e| format!("Invalid UTF-8: {}", e)) } ``` Add one function per action variant that calls `api_call` and parses the response into the result structs. ### A6: Write `src/lib.rs` Wire everything together: ```rust mod api; mod types; use types::Action; wit_bindgen::generate!({ world: "sandboxed-tool", path: "../../wit/tool.wit", }); struct Tool; impl exports::near::agent::tool::Guest for Tool { fn execute(req: exports::near::agent::tool::Request) -> exports::near::agent::tool::Response { match execute_inner(&req.params) { Ok(result) => exports::near::agent::tool::Response { output: Some(result), error: None, }, Err(e) => exports::near::agent::tool::Response { output: None, error: Some(e), }, } } fn schema() -> String { // Return JSON Schema matching the action enum todo!("Fill in JSON Schema") } fn description() -> String { "".to_string() } } fn execute_inner(params: &str) -> Result { // Check required secrets if !crate::near::agent::host::secret_exists("") { return Err(" not configured. Please add the '' secret.".to_string()); } let action: Action = serde_json::from_str(params).map_err(|e| format!("Invalid parameters: {}", e))?; crate::near::agent::host::log( crate::near::agent::host::LogLevel::Info, &format!("Executing action: {:?}", action), ); let result = match action { // Dispatch to api:: functions for each variant }; Ok(result) } export!(Tool); ``` Fill in the `schema()` with a proper JSON Schema using `oneOf` for each action variant. Reference `tools-src/slack/src/lib.rs` for the exact pattern. ### A7: Verify Run `cargo fmt` in the tool directory. If `cargo-component` is available, run `cargo component build --release` to verify the WASM compiles. --- ## Path B: Built-in Tool ### B1: Create the tool file Create `src/tools/builtin/.rs` implementing the `Tool` trait: ```rust use async_trait::async_trait; use crate::context::JobContext; use crate::tools::tool::{Tool, ToolError, ToolOutput}; pub struct Tool; #[async_trait] impl Tool for Tool { fn name(&self) -> &str { "" } fn description(&self) -> &str { "" } fn parameters_schema(&self) -> serde_json::Value { serde_json::json!({ "type": "object", "properties": { // Define parameters here }, "required": [] }) } async fn execute( &self, params: serde_json::Value, _ctx: &JobContext, ) -> Result { let start = std::time::Instant::now(); // Extract and validate parameters // Do the work // Return result Ok(ToolOutput::text("result", start.elapsed())) } fn requires_sanitization(&self) -> bool { false // Set true if tool processes external data } fn requires_approval(&self, _params: &serde_json::Value) -> crate::tools::tool::ApprovalRequirement { crate::tools::tool::ApprovalRequirement::Never // Set to UnlessAutoApproved or Always as needed } } ``` If the tool needs shared state (HTTP client, config), add a struct field and `new()` constructor: ```rust pub struct Tool { client: reqwest::Client, } impl Tool { pub fn new() -> Self { Self { client: reqwest::Client::builder() .timeout(std::time::Duration::from_secs(30)) .build() .expect("Failed to create HTTP client"), } } } ``` ### B2: Update `src/tools/builtin/mod.rs` Add the module declaration and pub use, keeping alphabetical order: ```rust mod ; pub use ::Tool; ``` ### B3: Update `src/tools/registry.rs` Add the import to the `use crate::tools::builtin::{...}` block and register the tool in the appropriate registration method: - If it's a core tool: add to `register_builtin_tools()` - If it needs shared state (workspace, context_manager, etc.): create a new `register__tools()` method or add to an existing one - Wire the new registration call in `src/main.rs` if a new method was created ### B4: Add tests Add a `mod tests {}` block at the bottom of the tool file: ```rust #[cfg(test)] mod tests { use super::*; use crate::context::JobContext; fn test_context() -> JobContext { JobContext::test_default() } #[tokio::test] async fn test__basic() { let tool = Tool::new(); let params = serde_json::json!({ /* test params */ }); let result = tool.execute(params, &test_context()).await; assert!(result.is_ok()); } #[tokio::test] async fn test__missing_params() { let tool = Tool::new(); let params = serde_json::json!({}); let result = tool.execute(params, &test_context()).await; assert!(matches!(result, Err(ToolError::InvalidParameters(_)))); } } ``` ### B5: Quality gate Run `cargo fmt` and `cargo clippy --all --benches --tests --examples --all-features`. Fix any issues. Run the new tests: `cargo test --lib -- builtin::::tests` --- ## Checklist Before finishing, verify: - [ ] Tool type chosen (WASM or built-in) and confirmed with user - [ ] All files created with correct structure - [ ] For WASM: capabilities.json declares all needed permissions (HTTP, secrets, auth) - [ ] For WASM: JSON Schema in `schema()` matches the action enum variants - [ ] For built-in: mod.rs updated with module + pub use - [ ] For built-in: registry.rs imports and registers the tool - [ ] For built-in: tests added and passing - [ ] `cargo fmt` clean - [ ] `cargo clippy` clean (for built-in) or `cargo component build` clean (for WASM)