agentzero_plugin_sdk/
lib.rs1pub mod prelude;
22
23use serde::{Deserialize, Serialize};
24
25#[derive(Debug, Clone, Deserialize)]
30pub struct ToolInput {
31 pub input: String,
33 pub workspace_root: String,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct ToolOutput {
42 pub output: String,
44 #[serde(skip_serializing_if = "Option::is_none")]
47 pub error: Option<String>,
48}
49
50impl ToolOutput {
51 pub fn success(output: impl Into<String>) -> Self {
53 Self {
54 output: output.into(),
55 error: None,
56 }
57 }
58
59 pub fn error(msg: impl Into<String>) -> Self {
61 Self {
62 output: String::new(),
63 error: Some(msg.into()),
64 }
65 }
66
67 pub fn with_warning(output: impl Into<String>, warning: impl Into<String>) -> Self {
69 Self {
70 output: output.into(),
71 error: Some(warning.into()),
72 }
73 }
74}
75
76#[inline]
80pub fn pack_ptr_len(ptr: u32, len: u32) -> i64 {
81 (ptr as i64) | ((len as i64) << 32)
82}
83
84#[inline]
90pub fn sdk_alloc(size: usize) -> *mut u8 {
91 let mut buf = vec![0u8; size];
92 let ptr = buf.as_mut_ptr();
93 std::mem::forget(buf);
94 ptr
95}
96
97#[inline]
103pub unsafe fn write_to_memory(dst: *mut u8, src: &[u8]) {
104 core::ptr::copy_nonoverlapping(src.as_ptr(), dst, src.len());
105}
106
107#[macro_export]
128macro_rules! declare_tool {
129 ($name:expr, $handler:ident) => {
130 #[no_mangle]
133 pub extern "C" fn az_alloc(size: i32) -> i32 {
134 $crate::sdk_alloc(size as usize) as i32
135 }
136
137 #[no_mangle]
140 pub extern "C" fn az_tool_name() -> i64 {
141 let name: &[u8] = $name.as_bytes();
142 let ptr = $crate::sdk_alloc(name.len());
143 unsafe {
144 $crate::write_to_memory(ptr, name);
145 }
146 $crate::pack_ptr_len(ptr as u32, name.len() as u32)
147 }
148
149 #[no_mangle]
152 pub extern "C" fn az_tool_execute(input_ptr: i32, input_len: i32) -> i64 {
153 let input_bytes =
155 unsafe { core::slice::from_raw_parts(input_ptr as *const u8, input_len as usize) };
156
157 let tool_input: $crate::ToolInput = match serde_json::from_slice(input_bytes) {
159 Ok(v) => v,
160 Err(e) => {
161 let err_output =
163 $crate::ToolOutput::error(format!("failed to parse input: {}", e));
164 let json = serde_json::to_vec(&err_output).unwrap_or_default();
165 let ptr = $crate::sdk_alloc(json.len());
166 unsafe {
167 $crate::write_to_memory(ptr, &json);
168 }
169 return $crate::pack_ptr_len(ptr as u32, json.len() as u32);
170 }
171 };
172
173 let output = $handler(tool_input);
175
176 let json = serde_json::to_vec(&output).unwrap_or_default();
178 let ptr = $crate::sdk_alloc(json.len());
179 unsafe {
180 $crate::write_to_memory(ptr, &json);
181 }
182 $crate::pack_ptr_len(ptr as u32, json.len() as u32)
183 }
184 };
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn tool_output_success() {
193 let out = ToolOutput::success("hello");
194 assert_eq!(out.output, "hello");
195 assert!(out.error.is_none());
196
197 let json = serde_json::to_string(&out).unwrap();
198 assert!(json.contains("\"output\":\"hello\""));
199 assert!(!json.contains("error"));
200 }
201
202 #[test]
203 fn tool_output_error() {
204 let out = ToolOutput::error("something broke");
205 assert!(out.output.is_empty());
206 assert_eq!(out.error.as_deref(), Some("something broke"));
207
208 let json = serde_json::to_string(&out).unwrap();
209 assert!(json.contains("\"error\":\"something broke\""));
210 }
211
212 #[test]
213 fn tool_output_with_warning() {
214 let out = ToolOutput::with_warning("result", "heads up");
215 assert_eq!(out.output, "result");
216 assert_eq!(out.error.as_deref(), Some("heads up"));
217 }
218
219 #[test]
220 fn tool_input_deserialize() {
221 let json = r#"{"input":"test data","workspace_root":"/tmp/ws"}"#;
222 let input: ToolInput = serde_json::from_str(json).unwrap();
223 assert_eq!(input.input, "test data");
224 assert_eq!(input.workspace_root, "/tmp/ws");
225 }
226
227 #[test]
228 fn pack_ptr_len_roundtrip() {
229 let ptr: u32 = 0x1000;
231 let len: u32 = 42;
232 let packed = pack_ptr_len(ptr, len);
233
234 let recovered_ptr = (packed & 0xFFFF_FFFF) as u32;
235 let recovered_len = ((packed >> 32) & 0xFFFF_FFFF) as u32;
236 assert_eq!(recovered_ptr, ptr);
237 assert_eq!(recovered_len, len);
238 }
239
240 #[test]
241 fn pack_ptr_len_zero() {
242 let packed = pack_ptr_len(0, 0);
243 assert_eq!(packed, 0);
244 }
245
246 #[test]
247 fn pack_ptr_len_max_values() {
248 let packed = pack_ptr_len(u32::MAX, u32::MAX);
249 let recovered_ptr = (packed & 0xFFFF_FFFF) as u32;
250 let recovered_len = ((packed >> 32) & 0xFFFF_FFFF) as u32;
251 assert_eq!(recovered_ptr, u32::MAX);
252 assert_eq!(recovered_len, u32::MAX);
253 }
254
255 #[test]
256 fn sdk_alloc_returns_valid_pointer() {
257 let ptr = sdk_alloc(64);
258 assert!(!ptr.is_null());
259 unsafe {
261 write_to_memory(ptr, &[0xAB; 64]);
262 assert_eq!(*ptr, 0xAB);
263 assert_eq!(*ptr.add(63), 0xAB);
264 }
265 }
266
267 #[test]
268 fn sdk_alloc_zero_size() {
269 let ptr = sdk_alloc(0);
271 let _ = ptr;
273 }
274
275 fn test_handler(input: ToolInput) -> ToolOutput {
277 ToolOutput::success(format!("echo: {}", input.input))
278 }
279
280 declare_tool!("test_plugin", test_handler);
281
282 #[test]
283 fn macro_generates_az_alloc() {
284 let ptr = az_alloc(32);
287 let _ = ptr;
289 }
290
291 #[cfg(target_pointer_width = "32")]
296 mod wasm_abi_tests {
297 use super::*;
298
299 #[test]
300 fn macro_generates_az_tool_name() {
301 let packed = az_tool_name();
302 let ptr = (packed & 0xFFFF_FFFF) as u32;
303 let len = ((packed >> 32) & 0xFFFF_FFFF) as u32;
304 assert_eq!(len, 11); let name = unsafe { core::slice::from_raw_parts(ptr as *const u8, len as usize) };
306 assert_eq!(name, b"test_plugin");
307 }
308
309 #[test]
310 fn macro_generates_az_tool_execute() {
311 let input_json = r#"{"input":"hello world","workspace_root":"/tmp"}"#;
312 let input_bytes = input_json.as_bytes();
313
314 let input_ptr = az_alloc(input_bytes.len() as i32);
315 unsafe {
316 core::ptr::copy_nonoverlapping(
317 input_bytes.as_ptr(),
318 input_ptr as *mut u8,
319 input_bytes.len(),
320 );
321 }
322
323 let packed = az_tool_execute(input_ptr, input_bytes.len() as i32);
324 let out_ptr = (packed & 0xFFFF_FFFF) as u32;
325 let out_len = ((packed >> 32) & 0xFFFF_FFFF) as u32;
326
327 let output_bytes =
328 unsafe { core::slice::from_raw_parts(out_ptr as *const u8, out_len as usize) };
329 let output: ToolOutput = serde_json::from_slice(output_bytes).unwrap();
330 assert_eq!(output.output, "echo: hello world");
331 assert!(output.error.is_none());
332 }
333
334 #[test]
335 fn macro_handles_invalid_input() {
336 let bad_json = b"not valid json";
337 let input_ptr = az_alloc(bad_json.len() as i32);
338 unsafe {
339 core::ptr::copy_nonoverlapping(
340 bad_json.as_ptr(),
341 input_ptr as *mut u8,
342 bad_json.len(),
343 );
344 }
345
346 let packed = az_tool_execute(input_ptr, bad_json.len() as i32);
347 let out_ptr = (packed & 0xFFFF_FFFF) as u32;
348 let out_len = ((packed >> 32) & 0xFFFF_FFFF) as u32;
349
350 let output_bytes =
351 unsafe { core::slice::from_raw_parts(out_ptr as *const u8, out_len as usize) };
352 let output: ToolOutput = serde_json::from_slice(output_bytes).unwrap();
353 assert!(output.output.is_empty());
354 assert!(output.error.as_deref().unwrap().contains("failed to parse"));
355 }
356 }
357}