Skip to main content

zeroclaw_plugins/
wasm_tool.rs

1//! Bridge between WASM plugins and the Tool trait.
2
3use crate::PluginPermission;
4use crate::runtime;
5use async_trait::async_trait;
6use serde_json::Value;
7use std::path::PathBuf;
8use zeroclaw_api::attribution::ToolKind;
9use zeroclaw_api::tool::{Tool, ToolResult};
10use zeroclaw_api::tool_attribution;
11
12tool_attribution!(WasmTool, ToolKind::Plugin);
13
14/// A tool backed by a WASM plugin function.
15pub struct WasmTool {
16    name: String,
17    description: String,
18    parameters_schema: Value,
19    wasm_path: PathBuf,
20    permissions: Vec<PluginPermission>,
21}
22
23impl WasmTool {
24    pub fn new(
25        name: String,
26        description: String,
27        parameters_schema: Value,
28        wasm_path: PathBuf,
29        permissions: Vec<PluginPermission>,
30    ) -> Self {
31        Self {
32            name,
33            description,
34            parameters_schema,
35            wasm_path,
36            permissions,
37        }
38    }
39
40    /// Create a WasmTool by loading metadata from the plugin's `tool_metadata` export.
41    /// Falls back to manifest-supplied values if the export is missing.
42    pub fn from_wasm(
43        wasm_path: PathBuf,
44        permissions: Vec<PluginPermission>,
45        fallback_name: String,
46        fallback_description: String,
47    ) -> Self {
48        // Try to load metadata from the WASM module itself.
49        let (name, description, schema) = match runtime::create_plugin(&wasm_path, &permissions) {
50            Ok(mut plugin) => match runtime::call_tool_metadata(&mut plugin) {
51                Ok(meta) => (meta.name, meta.description, meta.parameters_schema),
52                Err(e) => {
53                    ::zeroclaw_log::record!(
54                        DEBUG,
55                        ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note),
56                        &format!(
57                            "plugin at {} has no tool_metadata export ({e}), using fallback",
58                            wasm_path.display()
59                        )
60                    );
61                    (
62                        fallback_name.clone(),
63                        fallback_description.clone(),
64                        default_schema(),
65                    )
66                }
67            },
68            Err(e) => {
69                ::zeroclaw_log::record!(
70                    WARN,
71                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
72                        .with_outcome(::zeroclaw_log::EventOutcome::Unknown),
73                    &format!(
74                        "failed to load WASM plugin at {} for metadata: {e}",
75                        wasm_path.display()
76                    )
77                );
78                (
79                    fallback_name.clone(),
80                    fallback_description.clone(),
81                    default_schema(),
82                )
83            }
84        };
85
86        Self {
87            name,
88            description,
89            parameters_schema: schema,
90            wasm_path,
91            permissions,
92        }
93    }
94}
95
96/// The JSON Schema returned when a plugin lacks a `tool_metadata` export or fails
97/// to load at discovery time. Single source of truth so the fallback shape stays
98/// consistent across code paths.
99fn default_schema() -> Value {
100    serde_json::json!({
101        "type": "object",
102        "properties": {
103            "input": {
104                "type": "string",
105                "description": "Input for the plugin"
106            }
107        },
108        "required": ["input"]
109    })
110}
111
112#[async_trait]
113impl Tool for WasmTool {
114    fn name(&self) -> &str {
115        &self.name
116    }
117
118    fn description(&self) -> &str {
119        &self.description
120    }
121
122    fn parameters_schema(&self) -> Value {
123        self.parameters_schema.clone()
124    }
125
126    async fn execute(&self, args: Value) -> anyhow::Result<ToolResult> {
127        let wasm_path = self.wasm_path.clone();
128        let permissions = self.permissions.clone();
129        let args_json = serde_json::to_vec(&args)?;
130
131        // Extism Plugin is !Send, so we must create it inside spawn_blocking.
132        tokio::task::spawn_blocking(move || {
133            let mut plugin = runtime::create_plugin(&wasm_path, &permissions)?;
134            runtime::call_execute(&mut plugin, &args_json)
135        })
136        .await?
137    }
138}