use serde_json::Value;
use std::collections::HashMap;
#[cfg(target_arch = "wasm32")]
#[allow(dead_code)]
extern "C" {
fn wf_log(level: i32, msg_ptr: i32, msg_len: i32);
fn wf_set_response(ptr: i32, len: i32);
fn wf_get_host_response_len() -> i32;
fn wf_get_host_response(buf_ptr: i32, buf_len: i32) -> i32;
fn wf_generate_uuid() -> i32;
fn wf_current_time() -> i32;
fn wf_vault_get(key_ptr: i32, key_len: i32) -> i32;
fn wf_vault_set(key_ptr: i32, key_len: i32, val_ptr: i32, val_len: i32) -> i32;
fn wf_config_get(key_ptr: i32, key_len: i32) -> i32;
fn wf_http_fetch(
method_ptr: i32,
method_len: i32,
url_ptr: i32,
url_len: i32,
headers_ptr: i32,
headers_len: i32,
body_ptr: i32,
body_len: i32,
) -> i32;
fn wf_read_artifact(id_ptr: i32, id_len: i32) -> i32;
fn wf_stage_artifact(payload_ptr: i32, payload_len: i32) -> i32;
fn wf_tool_invoke(name_ptr: i32, name_len: i32, args_ptr: i32, args_len: i32) -> i32;
fn wf_tool_invoke_many(payload_ptr: i32, payload_len: i32) -> i32;
}
#[cfg(target_arch = "wasm32")]
fn read_host_response_string() -> Option<String> {
let len = unsafe { wf_get_host_response_len() };
if len <= 0 {
return None;
}
let mut buf = vec![0u8; len as usize];
let read = unsafe { wf_get_host_response(buf.as_mut_ptr() as i32, len) };
if read <= 0 {
return None;
}
buf.truncate(read as usize);
String::from_utf8(buf).ok()
}
#[derive(Debug, Clone)]
pub struct ToolInput {
pub data: Value,
pub tool_name: String,
pub agent_id: String,
pub user_id: Option<String>,
}
impl ToolInput {
pub fn from_json(json: &str) -> Option<Self> {
let v: Value = serde_json::from_str(json).ok()?;
Some(Self {
data: v["input"].clone(),
tool_name: v["tool_name"].as_str().unwrap_or("").to_string(),
agent_id: v["agent_id"].as_str().unwrap_or("").to_string(),
user_id: v["user_id"].as_str().map(|s| s.to_string()),
})
}
pub fn get_str(&self, key: &str) -> Option<&str> {
self.data.get(key).and_then(|v| v.as_str())
}
pub fn get_i64(&self, key: &str) -> Option<i64> {
self.data.get(key).and_then(|v| v.as_i64())
}
pub fn get_f64(&self, key: &str) -> Option<f64> {
self.data.get(key).and_then(|v| v.as_f64())
}
pub fn get_bool(&self, key: &str) -> Option<bool> {
self.data.get(key).and_then(|v| v.as_bool())
}
pub fn get(&self, key: &str) -> Option<&Value> {
self.data.get(key)
}
pub fn raw(&self) -> &Value {
&self.data
}
}
#[derive(Debug, Clone)]
pub struct ToolOutput {
success: bool,
result: Value,
error: Option<String>,
}
impl ToolOutput {
pub fn success(result: Value) -> Self {
Self {
success: true,
result,
error: None,
}
}
pub fn error(message: &str) -> Self {
Self {
success: false,
result: Value::Null,
error: Some(message.to_string()),
}
}
pub fn into_json(self) -> String {
let obj = serde_json::json!({
"success": self.success,
"result": self.result,
"error": self.error,
});
serde_json::to_string(&obj)
.unwrap_or_else(|_| r#"{"success":false,"error":"serialisation failed"}"#.to_string())
}
}
pub mod vault {
#[allow(unused_imports)]
use super::*;
pub fn get(key: &str) -> Option<String> {
#[cfg(target_arch = "wasm32")]
{
let bytes = key.as_bytes();
let result = unsafe { wf_vault_get(bytes.as_ptr() as i32, bytes.len() as i32) };
if result < 0 {
return None;
}
read_host_response_string()
}
#[cfg(not(target_arch = "wasm32"))]
{
let _ = key;
None
}
}
pub fn set(key: &str, value: &str) -> bool {
#[cfg(target_arch = "wasm32")]
{
let key_bytes = key.as_bytes();
let val_bytes = value.as_bytes();
let result = unsafe {
wf_vault_set(
key_bytes.as_ptr() as i32,
key_bytes.len() as i32,
val_bytes.as_ptr() as i32,
val_bytes.len() as i32,
)
};
result == 0
}
#[cfg(not(target_arch = "wasm32"))]
{
let _ = (key, value);
true
}
}
}
pub mod config {
#[allow(unused_imports)]
use super::*;
pub fn get(key: &str) -> Option<String> {
#[cfg(target_arch = "wasm32")]
{
let bytes = key.as_bytes();
let result = unsafe { wf_config_get(bytes.as_ptr() as i32, bytes.len() as i32) };
if result < 0 {
return None;
}
read_host_response_string()
}
#[cfg(not(target_arch = "wasm32"))]
{
let _ = key;
None
}
}
}
pub mod log {
#[allow(unused_imports)]
use super::*;
pub fn error(msg: &str) {
write(0, msg);
}
pub fn warn(msg: &str) {
write(1, msg);
}
pub fn info(msg: &str) {
write(2, msg);
}
pub fn debug(msg: &str) {
write(3, msg);
}
fn write(level: i32, msg: &str) {
#[cfg(target_arch = "wasm32")]
{
let bytes = msg.as_bytes();
unsafe {
super::wf_log(level, bytes.as_ptr() as i32, bytes.len() as i32);
}
}
#[cfg(not(target_arch = "wasm32"))]
{
let _ = (level, msg);
}
}
}
pub mod tools {
#[allow(unused_imports)]
use super::*;
pub fn invoke(name: &str, args: &Value) -> Result<Value, String> {
#[cfg(target_arch = "wasm32")]
{
let name_bytes = name.as_bytes();
let args_json = args.to_string();
let args_bytes = args_json.as_bytes();
let code = unsafe {
wf_tool_invoke(
name_bytes.as_ptr() as i32,
name_bytes.len() as i32,
args_bytes.as_ptr() as i32,
args_bytes.len() as i32,
)
};
match code {
0 => read_host_response_string()
.and_then(|s| serde_json::from_str(&s).ok())
.ok_or_else(|| "tool result unavailable".to_string()),
-2 => Err("the `tools` capability is not declared in Skill.toml".to_string()),
-4 => Err(
"denied by platform policy (declaration, grant, capability, or rule) — \
surface this failure; the runtime attaches the remedy for the agent"
.to_string(),
),
-5 => Err("tool-call budget exhausted for this invocation".to_string()),
_ => Err("tool invocation failed".to_string()),
}
}
#[cfg(not(target_arch = "wasm32"))]
{
let _ = (name, args);
Err("not running in the WASM runtime".to_string())
}
}
pub fn invoke_many(calls: &[(&str, Value)]) -> Result<Vec<Value>, String> {
#[cfg(target_arch = "wasm32")]
{
let payload = Value::Array(
calls
.iter()
.map(|(name, args)| serde_json::json!({"tool": name, "args": args}))
.collect(),
)
.to_string();
let bytes = payload.as_bytes();
let code = unsafe { wf_tool_invoke_many(bytes.as_ptr() as i32, bytes.len() as i32) };
match code {
0 => read_host_response_string()
.and_then(|s| serde_json::from_str(&s).ok())
.ok_or_else(|| "tool results unavailable".to_string()),
-2 => Err("the `tools` capability is not declared in Skill.toml".to_string()),
-4 => Err(
"denied by platform policy (grant, marker ban, or rule) — surface \
this failure; the runtime attaches the remedy for the agent"
.to_string(),
),
-5 => Err("tool-call budget exhausted for this invocation".to_string()),
_ => Err("tool invocation failed".to_string()),
}
}
#[cfg(not(target_arch = "wasm32"))]
{
let _ = calls;
Err("not running in the WASM runtime".to_string())
}
}
}
pub mod http {
#[allow(unused_imports)]
use super::*;
#[derive(Debug, Clone)]
pub struct FetchResponse {
pub status: i32,
pub body: String,
pub body_encoding: String,
pub headers: HashMap<String, String>,
}
impl FetchResponse {
pub fn json(&self) -> Option<Value> {
serde_json::from_str(&self.body).ok()
}
pub fn is_success(&self) -> bool {
(200..300).contains(&self.status)
}
pub fn is_base64(&self) -> bool {
self.body_encoding == "base64"
}
}
pub fn fetch(
method: &str,
url: &str,
headers: &[(&str, &str)],
body: Option<&str>,
) -> Option<FetchResponse> {
#[cfg(target_arch = "wasm32")]
{
let method_bytes = method.as_bytes();
let url_bytes = url.as_bytes();
let headers_map: HashMap<&str, &str> = headers.iter().copied().collect();
let headers_json = serde_json::to_string(&headers_map).unwrap_or_default();
let headers_bytes = headers_json.as_bytes();
let (body_bytes, body_len) = match body {
Some(b) => (b.as_bytes(), b.len()),
None => (&[] as &[u8], 0),
};
let result = unsafe {
wf_http_fetch(
method_bytes.as_ptr() as i32,
method_bytes.len() as i32,
url_bytes.as_ptr() as i32,
url_bytes.len() as i32,
headers_bytes.as_ptr() as i32,
headers_bytes.len() as i32,
body_bytes.as_ptr() as i32,
body_len as i32,
)
};
if result < 0 {
return None;
}
read_fetch_response()
}
#[cfg(not(target_arch = "wasm32"))]
{
let _ = (method, url, headers, body);
None
}
}
pub fn get(url: &str, headers: &[(&str, &str)]) -> Option<FetchResponse> {
fetch("GET", url, headers, None)
}
pub fn post(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
fetch("POST", url, headers, Some(body))
}
pub fn put(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
fetch("PUT", url, headers, Some(body))
}
pub fn delete(url: &str, headers: &[(&str, &str)]) -> Option<FetchResponse> {
fetch("DELETE", url, headers, None)
}
pub fn patch(url: &str, headers: &[(&str, &str)], body: &str) -> Option<FetchResponse> {
fetch("PATCH", url, headers, Some(body))
}
#[cfg(target_arch = "wasm32")]
fn read_fetch_response() -> Option<FetchResponse> {
let json_str = read_host_response_string()?;
let v: Value = serde_json::from_str(&json_str).ok()?;
Some(FetchResponse {
status: v["status"].as_i64().unwrap_or(0) as i32,
body: v["body"].as_str().unwrap_or("").to_string(),
body_encoding: v["body_encoding"].as_str().unwrap_or("utf8").to_string(),
headers: v["headers"]
.as_object()
.map(|m| {
m.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect()
})
.unwrap_or_default(),
})
}
}
pub mod artifact {
#[allow(unused_imports)]
use super::*;
#[cfg(target_arch = "wasm32")]
use base64::Engine;
#[derive(Debug, Clone)]
pub struct Artifact {
pub bytes: Vec<u8>,
pub mime: String,
pub size: usize,
}
#[derive(Debug, Clone)]
pub struct StagedArtifact {
pub artifact_id: String,
pub media_type: String,
pub filename: String,
pub bytes: u64,
}
impl StagedArtifact {
pub fn entry(&self) -> Value {
serde_json::json!({
"artifact_id": self.artifact_id,
"media_type": self.media_type,
"filename": self.filename,
})
}
}
pub fn read(id: &str) -> Option<Artifact> {
#[cfg(target_arch = "wasm32")]
{
let bytes = id.as_bytes();
let result = unsafe { wf_read_artifact(bytes.as_ptr() as i32, bytes.len() as i32) };
if result < 0 {
return None;
}
let v: Value = serde_json::from_str(&read_host_response_string()?).ok()?;
let decoded = base64::engine::general_purpose::STANDARD
.decode(v["bytes_base64"].as_str()?)
.ok()?;
Some(Artifact {
bytes: decoded,
mime: v["mime"]
.as_str()
.unwrap_or("application/octet-stream")
.to_string(),
size: v["size"].as_u64().unwrap_or(0) as usize,
})
}
#[cfg(not(target_arch = "wasm32"))]
{
let _ = id;
None
}
}
pub fn stage(bytes: &[u8], media_type: &str, filename: &str) -> Option<StagedArtifact> {
#[cfg(target_arch = "wasm32")]
{
let payload = serde_json::json!({
"bytes_base64": base64::engine::general_purpose::STANDARD.encode(bytes),
"media_type": media_type,
"filename": filename,
})
.to_string();
let pb = payload.as_bytes();
let result = unsafe { wf_stage_artifact(pb.as_ptr() as i32, pb.len() as i32) };
if result < 0 {
return None;
}
let v: Value = serde_json::from_str(&read_host_response_string()?).ok()?;
Some(StagedArtifact {
artifact_id: v["artifact_id"].as_str()?.to_string(),
media_type: v["media_type"].as_str().unwrap_or(media_type).to_string(),
filename: v["filename"].as_str().unwrap_or(filename).to_string(),
bytes: v["bytes"].as_u64().unwrap_or(bytes.len() as u64),
})
}
#[cfg(not(target_arch = "wasm32"))]
{
let _ = (bytes, media_type, filename);
None
}
}
const DELIVERY_KEY: &str = "generated_images";
pub fn attach(mut result: Value, files: &[StagedArtifact]) -> Value {
if let (Value::Object(map), false) = (&mut result, files.is_empty()) {
map.insert(
DELIVERY_KEY.to_string(),
Value::Array(files.iter().map(StagedArtifact::entry).collect()),
);
}
result
}
}
pub mod util {
#[allow(unused_imports)]
use super::*;
pub fn generate_uuid() -> String {
#[cfg(target_arch = "wasm32")]
{
let result = unsafe { super::wf_generate_uuid() };
if result < 0 {
return String::new();
}
read_host_response_string().unwrap_or_default()
}
#[cfg(not(target_arch = "wasm32"))]
{
format!("{:016x}", {
use std::time::SystemTime;
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0)
})
}
}
pub fn current_time() -> String {
#[cfg(target_arch = "wasm32")]
{
let result = unsafe { super::wf_current_time() };
if result < 0 {
return String::new();
}
read_host_response_string().unwrap_or_default()
}
#[cfg(not(target_arch = "wasm32"))]
{
use std::time::SystemTime;
let secs = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
format!("1970-01-01T00:00:{:02}Z", secs % 60)
}
}
}
#[doc(hidden)]
pub fn __run_tool_handler<F>(ptr: i32, len: i32, f: F) -> i32
where
F: FnOnce(ToolInput) -> ToolOutput,
{
let request_json = unsafe {
let slice = std::slice::from_raw_parts(ptr as *const u8, len as usize);
String::from_utf8_lossy(slice).into_owned()
};
let input = ToolInput::from_json(&request_json).unwrap_or_else(|| ToolInput {
data: Value::Null,
tool_name: String::new(),
agent_id: String::new(),
user_id: None,
});
let output = f(input);
let response_bytes = output.into_json().into_bytes();
let total = 4 + response_bytes.len();
let layout = std::alloc::Layout::from_size_align(total, 1).expect("invalid layout");
let out_ptr = unsafe { std::alloc::alloc(layout) };
unsafe {
let len_bytes = (response_bytes.len() as u32).to_le_bytes();
std::ptr::copy_nonoverlapping(len_bytes.as_ptr(), out_ptr, 4);
std::ptr::copy_nonoverlapping(
response_bytes.as_ptr(),
out_ptr.add(4),
response_bytes.len(),
);
}
out_ptr as i32
}
#[macro_export]
macro_rules! init {
() => {
#[no_mangle]
pub extern "C" fn alloc(size: i32) -> i32 {
let layout = std::alloc::Layout::from_size_align(size as usize, 1).unwrap();
unsafe { std::alloc::alloc(layout) as i32 }
}
};
}
#[macro_export]
macro_rules! tool {
($name:ident, |$input:ident : ToolInput| $body:expr) => {
#[no_mangle]
pub extern "C" fn $name(ptr: i32, len: i32) -> i32 {
$crate::__run_tool_handler(ptr, len, |$input: $crate::ToolInput| $body)
}
};
}
pub mod prelude {
pub use crate::artifact;
pub use crate::config;
pub use crate::http;
pub use crate::log;
pub use crate::tools;
pub use crate::util;
pub use crate::vault;
pub use crate::ToolInput;
pub use crate::ToolOutput;
pub use serde_json::{json, Value};
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn tool_input_parsing() {
let json = serde_json::to_string(&json!({
"tool_name": "weather.get_forecast",
"handler": "get_forecast",
"input": {"city": "London", "units": "metric"},
"agent_id": "agent-1",
}))
.unwrap();
let input = ToolInput::from_json(&json).unwrap();
assert_eq!(input.tool_name, "weather.get_forecast");
assert_eq!(input.agent_id, "agent-1");
assert_eq!(input.get_str("city"), Some("London"));
assert_eq!(input.get_str("units"), Some("metric"));
assert!(input.get_str("nonexistent").is_none());
}
#[test]
fn tool_input_accessors() {
let json = serde_json::to_string(&json!({
"input": {"count": 42, "ratio": 2.78, "active": true},
}))
.unwrap();
let input = ToolInput::from_json(&json).unwrap();
assert_eq!(input.get_i64("count"), Some(42));
assert_eq!(input.get_f64("ratio"), Some(2.78));
assert_eq!(input.get_bool("active"), Some(true));
}
#[test]
fn tool_input_missing_fields() {
let json = r#"{"input": {}}"#;
let input = ToolInput::from_json(json).unwrap();
assert_eq!(input.tool_name, "");
assert_eq!(input.agent_id, "");
assert_eq!(input.user_id, None);
}
#[test]
fn tool_input_parses_user_id() {
let json = serde_json::to_string(&json!({
"tool_name": "x",
"agent_id": "a",
"user_id": "alice",
"input": {},
}))
.unwrap();
let input = ToolInput::from_json(&json).unwrap();
assert_eq!(input.user_id.as_deref(), Some("alice"));
}
#[test]
fn tool_input_user_id_absent_when_unset() {
let json = r#"{"tool_name": "x", "agent_id": "a", "input": {}}"#;
let input = ToolInput::from_json(json).unwrap();
assert_eq!(input.user_id, None);
}
#[test]
fn tool_output_success() {
let output = ToolOutput::success(json!({"data": "test"}));
let json_str = output.into_json();
let parsed: Value = serde_json::from_str(&json_str).unwrap();
assert_eq!(parsed["success"], true);
assert_eq!(parsed["result"]["data"], "test");
assert!(parsed["error"].is_null());
}
#[test]
fn tool_output_error() {
let output = ToolOutput::error("something failed");
let json_str = output.into_json();
let parsed: Value = serde_json::from_str(&json_str).unwrap();
assert_eq!(parsed["success"], false);
assert!(parsed["result"].is_null());
assert_eq!(parsed["error"], "something failed");
}
#[test]
fn vault_get_noop_on_native() {
assert!(vault::get("any-key").is_none());
}
#[test]
fn vault_set_noop_on_native() {
assert!(vault::set("key", "value"));
}
#[test]
fn config_get_noop_on_native() {
assert!(config::get("any-key").is_none());
}
#[test]
fn http_get_noop_on_native() {
assert!(http::get("https://example.com", &[]).is_none());
}
#[test]
fn http_post_noop_on_native() {
assert!(http::post("https://example.com", &[], "{}").is_none());
}
#[test]
fn util_generate_uuid() {
let id = util::generate_uuid();
assert!(!id.is_empty());
}
#[test]
fn util_current_time() {
let time = util::current_time();
assert!(!time.is_empty());
}
#[test]
fn http_fetch_response_helpers() {
let resp = http::FetchResponse {
status: 200,
body: r#"{"key": "value"}"#.to_string(),
body_encoding: "utf8".to_string(),
headers: HashMap::new(),
};
assert!(resp.is_success());
assert!(!resp.is_base64());
let json = resp.json().unwrap();
assert_eq!(json["key"], "value");
let err_resp = http::FetchResponse {
status: 404,
body: "not found".to_string(),
body_encoding: "utf8".to_string(),
headers: HashMap::new(),
};
assert!(!err_resp.is_success());
let binary_resp = http::FetchResponse {
status: 200,
body: "aW1hZ2VkYXRh".to_string(),
body_encoding: "base64".to_string(),
headers: HashMap::new(),
};
assert!(binary_resp.is_base64());
}
#[test]
fn artifact_read_stage_noop_on_native() {
assert!(artifact::read("art_0").is_none());
assert!(artifact::stage(b"x", "text/plain", "x.txt").is_none());
}
#[test]
fn tools_invoke_noop_on_native() {
assert!(tools::invoke("echo.say", &json!({})).is_err());
assert!(tools::invoke_many(&[("echo.say", json!({}))]).is_err());
}
#[test]
fn artifact_attach_sets_delivery_key() {
let staged = artifact::StagedArtifact {
artifact_id: "art_00000000000000000000000000000000".to_string(),
media_type: "application/pdf".to_string(),
filename: "out.pdf".to_string(),
bytes: 42,
};
let out = artifact::attach(json!({ "rows": 3 }), std::slice::from_ref(&staged));
assert_eq!(out["rows"], 3);
let entries = out["generated_images"].as_array().unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0]["artifact_id"], staged.artifact_id);
assert_eq!(entries[0]["media_type"], "application/pdf");
assert_eq!(entries[0]["filename"], "out.pdf");
}
#[test]
fn artifact_attach_empty_is_unchanged() {
let out = artifact::attach(json!({ "rows": 3 }), &[]);
assert!(out.get("generated_images").is_none());
}
}