1use serde_json::Value;
42use std::collections::HashMap;
43
44#[cfg(target_arch = "wasm32")]
50#[allow(dead_code)]
51extern "C" {
52 fn wf_log(level: i32, msg_ptr: i32, msg_len: i32);
53 fn wf_set_response(ptr: i32, len: i32);
54 fn wf_get_host_response_len() -> i32;
55 fn wf_get_host_response(buf_ptr: i32, buf_len: i32) -> i32;
56 fn wf_generate_uuid() -> i32;
57 fn wf_current_time() -> i32;
58 fn wf_vault_get(key_ptr: i32, key_len: i32) -> i32;
59 fn wf_vault_set(key_ptr: i32, key_len: i32, val_ptr: i32, val_len: i32) -> i32;
60 fn wf_config_get(key_ptr: i32, key_len: i32) -> i32;
61 fn wf_http_fetch(
62 method_ptr: i32,
63 method_len: i32,
64 url_ptr: i32,
65 url_len: i32,
66 headers_ptr: i32,
67 headers_len: i32,
68 body_ptr: i32,
69 body_len: i32,
70 ) -> i32;
71 fn wf_read_artifact(id_ptr: i32, id_len: i32) -> i32;
72 fn wf_stage_artifact(payload_ptr: i32, payload_len: i32) -> i32;
73 fn wf_tool_invoke(name_ptr: i32, name_len: i32, args_ptr: i32, args_len: i32) -> i32;
74}
75
76#[cfg(target_arch = "wasm32")]
79fn read_host_response_string() -> Option<String> {
80 let len = unsafe { wf_get_host_response_len() };
81 if len <= 0 {
82 return None;
83 }
84 let mut buf = vec![0u8; len as usize];
85 let read = unsafe { wf_get_host_response(buf.as_mut_ptr() as i32, len) };
86 if read <= 0 {
87 return None;
88 }
89 buf.truncate(read as usize);
90 String::from_utf8(buf).ok()
91}
92
93#[derive(Debug, Clone)]
99pub struct ToolInput {
100 pub data: Value,
102 pub tool_name: String,
104 pub agent_id: String,
106 pub user_id: Option<String>,
113}
114
115impl ToolInput {
116 pub fn from_json(json: &str) -> Option<Self> {
118 let v: Value = serde_json::from_str(json).ok()?;
119 Some(Self {
120 data: v["input"].clone(),
121 tool_name: v["tool_name"].as_str().unwrap_or("").to_string(),
122 agent_id: v["agent_id"].as_str().unwrap_or("").to_string(),
123 user_id: v["user_id"].as_str().map(|s| s.to_string()),
124 })
125 }
126
127 pub fn get_str(&self, key: &str) -> Option<&str> {
129 self.data.get(key).and_then(|v| v.as_str())
130 }
131
132 pub fn get_i64(&self, key: &str) -> Option<i64> {
134 self.data.get(key).and_then(|v| v.as_i64())
135 }
136
137 pub fn get_f64(&self, key: &str) -> Option<f64> {
139 self.data.get(key).and_then(|v| v.as_f64())
140 }
141
142 pub fn get_bool(&self, key: &str) -> Option<bool> {
144 self.data.get(key).and_then(|v| v.as_bool())
145 }
146
147 pub fn get(&self, key: &str) -> Option<&Value> {
149 self.data.get(key)
150 }
151
152 pub fn raw(&self) -> &Value {
154 &self.data
155 }
156}
157
158#[derive(Debug, Clone)]
165pub struct ToolOutput {
166 success: bool,
167 result: Value,
168 error: Option<String>,
169}
170
171impl ToolOutput {
172 pub fn success(result: Value) -> Self {
174 Self {
175 success: true,
176 result,
177 error: None,
178 }
179 }
180
181 pub fn error(message: &str) -> Self {
183 Self {
184 success: false,
185 result: Value::Null,
186 error: Some(message.to_string()),
187 }
188 }
189
190 pub fn into_json(self) -> String {
192 let obj = serde_json::json!({
193 "success": self.success,
194 "result": self.result,
195 "error": self.error,
196 });
197 serde_json::to_string(&obj)
198 .unwrap_or_else(|_| r#"{"success":false,"error":"serialisation failed"}"#.to_string())
199 }
200}
201
202pub mod vault {
211 #[allow(unused_imports)]
212 use super::*;
213
214 pub fn get(key: &str) -> Option<String> {
220 #[cfg(target_arch = "wasm32")]
221 {
222 let bytes = key.as_bytes();
223 let result = unsafe { wf_vault_get(bytes.as_ptr() as i32, bytes.len() as i32) };
224 if result < 0 {
225 return None;
226 }
227 read_host_response_string()
228 }
229 #[cfg(not(target_arch = "wasm32"))]
230 {
231 let _ = key;
232 None
233 }
234 }
235
236 pub fn set(key: &str, value: &str) -> bool {
242 #[cfg(target_arch = "wasm32")]
243 {
244 let key_bytes = key.as_bytes();
245 let val_bytes = value.as_bytes();
246 let result = unsafe {
247 wf_vault_set(
248 key_bytes.as_ptr() as i32,
249 key_bytes.len() as i32,
250 val_bytes.as_ptr() as i32,
251 val_bytes.len() as i32,
252 )
253 };
254 result == 0
255 }
256 #[cfg(not(target_arch = "wasm32"))]
257 {
258 let _ = (key, value);
259 true
260 }
261 }
262}
263
264pub mod config {
273 #[allow(unused_imports)]
274 use super::*;
275
276 pub fn get(key: &str) -> Option<String> {
282 #[cfg(target_arch = "wasm32")]
283 {
284 let bytes = key.as_bytes();
285 let result = unsafe { wf_config_get(bytes.as_ptr() as i32, bytes.len() as i32) };
286 if result < 0 {
287 return None;
288 }
289 read_host_response_string()
290 }
291 #[cfg(not(target_arch = "wasm32"))]
292 {
293 let _ = key;
294 None
295 }
296 }
297}
298
299pub mod log {
305 #[allow(unused_imports)]
306 use super::*;
307
308 pub fn error(msg: &str) {
310 write(0, msg);
311 }
312
313 pub fn warn(msg: &str) {
315 write(1, msg);
316 }
317
318 pub fn info(msg: &str) {
320 write(2, msg);
321 }
322
323 pub fn debug(msg: &str) {
325 write(3, msg);
326 }
327
328 fn write(level: i32, msg: &str) {
329 #[cfg(target_arch = "wasm32")]
330 {
331 let bytes = msg.as_bytes();
332 unsafe {
333 super::wf_log(level, bytes.as_ptr() as i32, bytes.len() as i32);
334 }
335 }
336 #[cfg(not(target_arch = "wasm32"))]
337 {
338 let _ = (level, msg);
339 }
340 }
341}
342
343pub mod tools {
349 #[allow(unused_imports)]
350 use super::*;
351
352 pub fn invoke(name: &str, args: &Value) -> Result<Value, String> {
371 #[cfg(target_arch = "wasm32")]
372 {
373 let name_bytes = name.as_bytes();
374 let args_json = args.to_string();
375 let args_bytes = args_json.as_bytes();
376 let code = unsafe {
377 wf_tool_invoke(
378 name_bytes.as_ptr() as i32,
379 name_bytes.len() as i32,
380 args_bytes.as_ptr() as i32,
381 args_bytes.len() as i32,
382 )
383 };
384 match code {
385 0 => read_host_response_string()
386 .and_then(|s| serde_json::from_str(&s).ok())
387 .ok_or_else(|| "tool result unavailable".to_string()),
388 -2 => Err("the `tools` capability is not declared in Skill.toml".to_string()),
389 -4 => Err(
390 "denied by platform policy (declaration, grant, capability, or rule) — \
391 surface this failure; the runtime attaches the remedy for the agent"
392 .to_string(),
393 ),
394 -5 => Err("tool-call budget exhausted for this invocation".to_string()),
395 _ => Err("tool invocation failed".to_string()),
396 }
397 }
398 #[cfg(not(target_arch = "wasm32"))]
399 {
400 let _ = (name, args);
401 Err("not running in the WASM runtime".to_string())
402 }
403 }
404}
405
406pub mod http {
407 #[allow(unused_imports)]
408 use super::*;
409
410 #[derive(Debug, Clone)]
412 pub struct FetchResponse {
413 pub status: i32,
415 pub body: String,
417 pub body_encoding: String,
419 pub headers: HashMap<String, String>,
421 }
422
423 impl FetchResponse {
424 pub fn json(&self) -> Option<Value> {
426 serde_json::from_str(&self.body).ok()
427 }
428
429 pub fn is_success(&self) -> bool {
431 (200..300).contains(&self.status)
432 }
433
434 pub fn is_base64(&self) -> bool {
436 self.body_encoding == "base64"
437 }
438 }
439
440 pub fn fetch(
442 method: &str,
443 url: &str,
444 headers: &[(&str, &str)],
445 body: Option<&str>,
446 ) -> Option<FetchResponse> {
447 #[cfg(target_arch = "wasm32")]
448 {
449 let method_bytes = method.as_bytes();
450 let url_bytes = url.as_bytes();
451 let headers_map: HashMap<&str, &str> = headers.iter().copied().collect();
452 let headers_json = serde_json::to_string(&headers_map).unwrap_or_default();
453 let headers_bytes = headers_json.as_bytes();
454 let (body_bytes, body_len) = match body {
455 Some(b) => (b.as_bytes(), b.len()),
456 None => (&[] as &[u8], 0),
457 };
458
459 let result = unsafe {
460 wf_http_fetch(
461 method_bytes.as_ptr() as i32,
462 method_bytes.len() as i32,
463 url_bytes.as_ptr() as i32,
464 url_bytes.len() as i32,
465 headers_bytes.as_ptr() as i32,
466 headers_bytes.len() as i32,
467 body_bytes.as_ptr() as i32,
468 body_len as i32,
469 )
470 };
471
472 if result < 0 {
473 return None;
474 }
475
476 read_fetch_response()
477 }
478 #[cfg(not(target_arch = "wasm32"))]
479 {
480 let _ = (method, url, headers, body);
481 None
482 }
483 }
484
485 pub fn get(url: &str, headers: &[(&str, &str)]) -> Option<FetchResponse> {
487 fetch("GET", url, headers, None)
488 }
489
490 pub fn post(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
492 fetch("POST", url, headers, Some(body))
493 }
494
495 pub fn put(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
497 fetch("PUT", url, headers, Some(body))
498 }
499
500 pub fn delete(url: &str, headers: &[(&str, &str)]) -> Option<FetchResponse> {
502 fetch("DELETE", url, headers, None)
503 }
504
505 pub fn patch(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
507 fetch("PATCH", url, headers, Some(body))
508 }
509
510 #[cfg(target_arch = "wasm32")]
511 fn read_fetch_response() -> Option<FetchResponse> {
512 let json_str = read_host_response_string()?;
513 let v: Value = serde_json::from_str(&json_str).ok()?;
514 Some(FetchResponse {
515 status: v["status"].as_i64().unwrap_or(0) as i32,
516 body: v["body"].as_str().unwrap_or("").to_string(),
517 body_encoding: v["body_encoding"].as_str().unwrap_or("utf8").to_string(),
518 headers: v["headers"]
519 .as_object()
520 .map(|m| {
521 m.iter()
522 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
523 .collect()
524 })
525 .unwrap_or_default(),
526 })
527 }
528}
529
530pub mod artifact {
540 #[allow(unused_imports)]
541 use super::*;
542 #[cfg(target_arch = "wasm32")]
543 use base64::Engine;
544
545 #[derive(Debug, Clone)]
547 pub struct Artifact {
548 pub bytes: Vec<u8>,
549 pub mime: String,
550 pub size: usize,
551 }
552
553 #[derive(Debug, Clone)]
556 pub struct StagedArtifact {
557 pub artifact_id: String,
558 pub media_type: String,
559 pub filename: String,
560 pub bytes: u64,
561 }
562
563 impl StagedArtifact {
564 pub fn entry(&self) -> Value {
566 serde_json::json!({
567 "artifact_id": self.artifact_id,
568 "media_type": self.media_type,
569 "filename": self.filename,
570 })
571 }
572 }
573
574 pub fn read(id: &str) -> Option<Artifact> {
577 #[cfg(target_arch = "wasm32")]
578 {
579 let bytes = id.as_bytes();
580 let result = unsafe { wf_read_artifact(bytes.as_ptr() as i32, bytes.len() as i32) };
581 if result < 0 {
582 return None;
583 }
584 let v: Value = serde_json::from_str(&read_host_response_string()?).ok()?;
585 let decoded = base64::engine::general_purpose::STANDARD
586 .decode(v["bytes_base64"].as_str()?)
587 .ok()?;
588 Some(Artifact {
589 bytes: decoded,
590 mime: v["mime"]
591 .as_str()
592 .unwrap_or("application/octet-stream")
593 .to_string(),
594 size: v["size"].as_u64().unwrap_or(0) as usize,
595 })
596 }
597 #[cfg(not(target_arch = "wasm32"))]
598 {
599 let _ = id;
600 None
601 }
602 }
603
604 pub fn stage(bytes: &[u8], media_type: &str, filename: &str) -> Option<StagedArtifact> {
607 #[cfg(target_arch = "wasm32")]
608 {
609 let payload = serde_json::json!({
610 "bytes_base64": base64::engine::general_purpose::STANDARD.encode(bytes),
611 "media_type": media_type,
612 "filename": filename,
613 })
614 .to_string();
615 let pb = payload.as_bytes();
616 let result = unsafe { wf_stage_artifact(pb.as_ptr() as i32, pb.len() as i32) };
617 if result < 0 {
618 return None;
619 }
620 let v: Value = serde_json::from_str(&read_host_response_string()?).ok()?;
621 Some(StagedArtifact {
622 artifact_id: v["artifact_id"].as_str()?.to_string(),
623 media_type: v["media_type"].as_str().unwrap_or(media_type).to_string(),
624 filename: v["filename"].as_str().unwrap_or(filename).to_string(),
625 bytes: v["bytes"].as_u64().unwrap_or(bytes.len() as u64),
626 })
627 }
628 #[cfg(not(target_arch = "wasm32"))]
629 {
630 let _ = (bytes, media_type, filename);
631 None
632 }
633 }
634
635 const DELIVERY_KEY: &str = "generated_images";
637
638 pub fn attach(mut result: Value, files: &[StagedArtifact]) -> Value {
642 if let (Value::Object(map), false) = (&mut result, files.is_empty()) {
643 map.insert(
644 DELIVERY_KEY.to_string(),
645 Value::Array(files.iter().map(StagedArtifact::entry).collect()),
646 );
647 }
648 result
649 }
650}
651
652pub mod util {
656 #[allow(unused_imports)]
657 use super::*;
658
659 pub fn generate_uuid() -> String {
661 #[cfg(target_arch = "wasm32")]
662 {
663 let result = unsafe { super::wf_generate_uuid() };
664 if result < 0 {
665 return String::new();
666 }
667 read_host_response_string().unwrap_or_default()
668 }
669
670 #[cfg(not(target_arch = "wasm32"))]
671 {
672 format!("{:016x}", {
674 use std::time::SystemTime;
675 SystemTime::now()
676 .duration_since(SystemTime::UNIX_EPOCH)
677 .map(|d| d.as_nanos() as u64)
678 .unwrap_or(0)
679 })
680 }
681 }
682
683 pub fn current_time() -> String {
685 #[cfg(target_arch = "wasm32")]
686 {
687 let result = unsafe { super::wf_current_time() };
688 if result < 0 {
689 return String::new();
690 }
691 read_host_response_string().unwrap_or_default()
692 }
693
694 #[cfg(not(target_arch = "wasm32"))]
695 {
696 use std::time::SystemTime;
697 let secs = SystemTime::now()
698 .duration_since(SystemTime::UNIX_EPOCH)
699 .map(|d| d.as_secs())
700 .unwrap_or(0);
701 format!("1970-01-01T00:00:{:02}Z", secs % 60)
702 }
703 }
704}
705
706#[doc(hidden)]
710pub fn __run_tool_handler<F>(ptr: i32, len: i32, f: F) -> i32
711where
712 F: FnOnce(ToolInput) -> ToolOutput,
713{
714 let request_json = unsafe {
716 let slice = std::slice::from_raw_parts(ptr as *const u8, len as usize);
717 String::from_utf8_lossy(slice).into_owned()
718 };
719
720 let input = ToolInput::from_json(&request_json).unwrap_or_else(|| ToolInput {
722 data: Value::Null,
723 tool_name: String::new(),
724 agent_id: String::new(),
725 user_id: None,
726 });
727
728 let output = f(input);
730 let response_bytes = output.into_json().into_bytes();
731
732 let total = 4 + response_bytes.len();
734 let layout = std::alloc::Layout::from_size_align(total, 1).expect("invalid layout");
735 let out_ptr = unsafe { std::alloc::alloc(layout) };
736
737 unsafe {
738 let len_bytes = (response_bytes.len() as u32).to_le_bytes();
739 std::ptr::copy_nonoverlapping(len_bytes.as_ptr(), out_ptr, 4);
740 std::ptr::copy_nonoverlapping(
741 response_bytes.as_ptr(),
742 out_ptr.add(4),
743 response_bytes.len(),
744 );
745 }
746
747 out_ptr as i32
748}
749
750#[macro_export]
761macro_rules! init {
762 () => {
763 #[no_mangle]
764 pub extern "C" fn alloc(size: i32) -> i32 {
765 let layout = std::alloc::Layout::from_size_align(size as usize, 1).unwrap();
766 unsafe { std::alloc::alloc(layout) as i32 }
767 }
768 };
769}
770
771#[macro_export]
788macro_rules! tool {
789 ($name:ident, |$input:ident : ToolInput| $body:expr) => {
790 #[no_mangle]
791 pub extern "C" fn $name(ptr: i32, len: i32) -> i32 {
792 $crate::__run_tool_handler(ptr, len, |$input: $crate::ToolInput| $body)
793 }
794 };
795}
796
797pub mod prelude {
805 pub use crate::artifact;
806 pub use crate::config;
807 pub use crate::http;
808 pub use crate::log;
809 pub use crate::tools;
810 pub use crate::util;
811 pub use crate::vault;
812 pub use crate::ToolInput;
813 pub use crate::ToolOutput;
814 pub use serde_json::{json, Value};
815}
816
817#[cfg(test)]
820mod tests {
821 use super::*;
822 use serde_json::json;
823
824 #[test]
825 fn tool_input_parsing() {
826 let json = serde_json::to_string(&json!({
827 "tool_name": "weather.get_forecast",
828 "handler": "get_forecast",
829 "input": {"city": "London", "units": "metric"},
830 "agent_id": "agent-1",
831 }))
832 .unwrap();
833
834 let input = ToolInput::from_json(&json).unwrap();
835 assert_eq!(input.tool_name, "weather.get_forecast");
836 assert_eq!(input.agent_id, "agent-1");
837 assert_eq!(input.get_str("city"), Some("London"));
838 assert_eq!(input.get_str("units"), Some("metric"));
839 assert!(input.get_str("nonexistent").is_none());
840 }
841
842 #[test]
843 fn tool_input_accessors() {
844 let json = serde_json::to_string(&json!({
845 "input": {"count": 42, "ratio": 2.78, "active": true},
846 }))
847 .unwrap();
848
849 let input = ToolInput::from_json(&json).unwrap();
850 assert_eq!(input.get_i64("count"), Some(42));
851 assert_eq!(input.get_f64("ratio"), Some(2.78));
852 assert_eq!(input.get_bool("active"), Some(true));
853 }
854
855 #[test]
856 fn tool_input_missing_fields() {
857 let json = r#"{"input": {}}"#;
858 let input = ToolInput::from_json(json).unwrap();
859 assert_eq!(input.tool_name, "");
860 assert_eq!(input.agent_id, "");
861 assert_eq!(input.user_id, None);
862 }
863
864 #[test]
865 fn tool_input_parses_user_id() {
866 let json = serde_json::to_string(&json!({
867 "tool_name": "x",
868 "agent_id": "a",
869 "user_id": "alice",
870 "input": {},
871 }))
872 .unwrap();
873 let input = ToolInput::from_json(&json).unwrap();
874 assert_eq!(input.user_id.as_deref(), Some("alice"));
875 }
876
877 #[test]
878 fn tool_input_user_id_absent_when_unset() {
879 let json = r#"{"tool_name": "x", "agent_id": "a", "input": {}}"#;
880 let input = ToolInput::from_json(json).unwrap();
881 assert_eq!(input.user_id, None);
882 }
883
884 #[test]
885 fn tool_output_success() {
886 let output = ToolOutput::success(json!({"data": "test"}));
887 let json_str = output.into_json();
888 let parsed: Value = serde_json::from_str(&json_str).unwrap();
889 assert_eq!(parsed["success"], true);
890 assert_eq!(parsed["result"]["data"], "test");
891 assert!(parsed["error"].is_null());
892 }
893
894 #[test]
895 fn tool_output_error() {
896 let output = ToolOutput::error("something failed");
897 let json_str = output.into_json();
898 let parsed: Value = serde_json::from_str(&json_str).unwrap();
899 assert_eq!(parsed["success"], false);
900 assert!(parsed["result"].is_null());
901 assert_eq!(parsed["error"], "something failed");
902 }
903
904 #[test]
905 fn vault_get_noop_on_native() {
906 assert!(vault::get("any-key").is_none());
907 }
908
909 #[test]
910 fn vault_set_noop_on_native() {
911 assert!(vault::set("key", "value"));
912 }
913
914 #[test]
915 fn config_get_noop_on_native() {
916 assert!(config::get("any-key").is_none());
917 }
918
919 #[test]
920 fn http_get_noop_on_native() {
921 assert!(http::get("https://example.com", &[]).is_none());
922 }
923
924 #[test]
925 fn http_post_noop_on_native() {
926 assert!(http::post("https://example.com", &[], "{}").is_none());
927 }
928
929 #[test]
930 fn util_generate_uuid() {
931 let id = util::generate_uuid();
932 assert!(!id.is_empty());
933 }
934
935 #[test]
936 fn util_current_time() {
937 let time = util::current_time();
938 assert!(!time.is_empty());
939 }
940
941 #[test]
942 fn http_fetch_response_helpers() {
943 let resp = http::FetchResponse {
944 status: 200,
945 body: r#"{"key": "value"}"#.to_string(),
946 body_encoding: "utf8".to_string(),
947 headers: HashMap::new(),
948 };
949 assert!(resp.is_success());
950 assert!(!resp.is_base64());
951 let json = resp.json().unwrap();
952 assert_eq!(json["key"], "value");
953
954 let err_resp = http::FetchResponse {
955 status: 404,
956 body: "not found".to_string(),
957 body_encoding: "utf8".to_string(),
958 headers: HashMap::new(),
959 };
960 assert!(!err_resp.is_success());
961
962 let binary_resp = http::FetchResponse {
963 status: 200,
964 body: "aW1hZ2VkYXRh".to_string(),
965 body_encoding: "base64".to_string(),
966 headers: HashMap::new(),
967 };
968 assert!(binary_resp.is_base64());
969 }
970
971 #[test]
972 fn artifact_read_stage_noop_on_native() {
973 assert!(artifact::read("art_0").is_none());
974 assert!(artifact::stage(b"x", "text/plain", "x.txt").is_none());
975 }
976
977 #[test]
978 fn artifact_attach_sets_delivery_key() {
979 let staged = artifact::StagedArtifact {
980 artifact_id: "art_00000000000000000000000000000000".to_string(),
981 media_type: "application/pdf".to_string(),
982 filename: "out.pdf".to_string(),
983 bytes: 42,
984 };
985 let out = artifact::attach(json!({ "rows": 3 }), std::slice::from_ref(&staged));
986 assert_eq!(out["rows"], 3);
987 let entries = out["generated_images"].as_array().unwrap();
988 assert_eq!(entries.len(), 1);
989 assert_eq!(entries[0]["artifact_id"], staged.artifact_id);
990 assert_eq!(entries[0]["media_type"], "application/pdf");
991 assert_eq!(entries[0]["filename"], "out.pdf");
992 }
993
994 #[test]
995 fn artifact_attach_empty_is_unchanged() {
996 let out = artifact::attach(json!({ "rows": 3 }), &[]);
997 assert!(out.get("generated_images").is_none());
998 }
999}