WebAssembly Integration Guide

Complete guide to WebAssembly integration in Caxton, including agent development, sandboxing, and performance optimization.

WebAssembly Integration Guide

WebAssembly (WASM) provides the secure sandboxing foundation for Caxton’s multi-agent system. This comprehensive guide covers everything from basic agent development to advanced optimization techniques and security considerations.

Overview

Caxton uses WebAssembly to create isolated execution environments for agents, providing:

  • Security: Memory-safe execution with controlled system access
  • Performance: Near-native execution speed with minimal overhead
  • Portability: Write agents in any language that compiles to WASM
  • Resource Control: Fine-grained CPU and memory limits
  • Multi-tenancy: Safe execution of untrusted code

WebAssembly Runtime Architecture

Execution Model

┌─────────────────────────────────────────────────────────┐
│ Caxton Host Runtime (Rust)                            │
│  ┌─────────────────────────────────────────────────┐   │
│  │ WASM Runtime (Wasmtime/Wasmer)                  │   │
│  │  ┌──────────────┐  ┌──────────────┐            │   │
│  │  │ Agent WASM   │  │ Agent WASM   │  ...       │   │
│  │  │   Instance   │  │   Instance   │            │   │
│  │  │              │  │              │            │   │
│  │  │ [Linear Mem] │  │ [Linear Mem] │            │   │
│  │  │ [Function    │  │ [Function    │            │   │
│  │  │  Imports]    │  │  Imports]    │            │   │
│  │  │ [Function    │  │ [Function    │            │   │
│  │  │  Exports]    │  │  Exports]    │            │   │
│  │  └──────────────┘  └──────────────┘            │   │
│  └─────────────────────────────────────────────────┘   │
│                                                         │
│  ┌─────────────────────────────────────────────────┐   │
│  │ Host Interface Layer                            │   │
│  │ • Message Bus                                   │   │
│  │ • Resource Manager                              │   │
│  │ • Security Monitor                              │   │
│  │ • Performance Profiler                         │   │
│  └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

Security Boundaries

Each WASM agent runs in a completely isolated sandbox:

  • Memory Isolation: Agents cannot access each other’s memory
  • Function Isolation: Only explicitly exported functions are accessible
  • System Call Mediation: All system interactions go through controlled host functions
  • Resource Limits: CPU time, memory usage, and network access are strictly controlled

Agent Development

Supported Languages

Caxton supports agents written in any language that compiles to WebAssembly:

# Cargo.toml
[package]
name = "my-caxton-agent"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
caxton-sdk = "0.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
wasm-bindgen = "0.2"

[dependencies.web-sys]
version = "0.3"
features = [
  "console",
]
// src/lib.rs
use caxton_sdk::*;
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;

#[derive(Serialize, Deserialize)]
struct ProcessRequest {
    data: String,
    operation: String,
}

#[derive(Serialize, Deserialize)]
struct ProcessResponse {
    result: String,
    timestamp: u64,
}

// Agent lifecycle hooks
#[wasm_bindgen]
pub fn agent_init() -> i32 {
    console_log("Agent initializing...");

    // Register message handlers
    register_handler("process_data", handle_process_data);
    register_handler("get_status", handle_get_status);

    0 // Success
}

#[wasm_bindgen]
pub fn agent_shutdown() {
    console_log("Agent shutting down...");
    // Cleanup resources
}

// Message handler
#[wasm_bindgen]
pub fn handle_message(msg_ptr: *const u8, msg_len: usize) -> i32 {
    let msg_bytes = unsafe {
        std::slice::from_raw_parts(msg_ptr, msg_len)
    };

    match serde_json::from_slice::<FipaMessage>(msg_bytes) {
        Ok(message) => {
            match message.performative.as_str() {
                "request" => handle_request(message),
                "inform" => handle_inform(message),
                _ => {
                    send_not_understood(&message);
                    1
                }
            }
        }
        Err(e) => {
            console_error(&format!("Failed to parse message: {}", e));
            1
        }
    }
}

fn handle_request(message: FipaMessage) -> i32 {
    match message.content.get("action").and_then(|v| v.as_str()) {
        Some("process_data") => {
            if let Ok(request) = serde_json::from_value::<ProcessRequest>(
                message.content["data"].clone()
            ) {
                let response = process_data(request);
                send_inform_response(&message, &response);
                0
            } else {
                send_failure(&message, "Invalid request format");
                1
            }
        }
        Some("get_status") => {
            let status = get_agent_status();
            send_inform_response(&message, &status);
            0
        }
        _ => {
            send_not_understood(&message);
            1
        }
    }
}

fn process_data(request: ProcessRequest) -> ProcessResponse {
    // Implement your processing logic here
    let result = match request.operation.as_str() {
        "uppercase" => request.data.to_uppercase(),
        "lowercase" => request.data.to_lowercase(),
        "reverse" => request.data.chars().rev().collect(),
        _ => format!("Unknown operation: {}", request.operation),
    };

    ProcessResponse {
        result,
        timestamp: current_timestamp(),
    }
}

AssemblyScript

// assembly/index.ts
import { JSON } from "assemblyscript-json";
import { console } from "./console";

class ProcessRequest {
    data: string = "";
    operation: string = "";
}

class ProcessResponse {
    result: string = "";
    timestamp: u64 = 0;
}

export function agent_init(): i32 {
    console.log("AssemblyScript agent initializing...");
    return 0;
}

export function handle_message(msgPtr: i32, msgLen: i32): i32 {
    // Read message from memory
    const msgBuffer = new ArrayBuffer(msgLen);
    memory.copy(changetype<usize>(msgBuffer), msgPtr, msgLen);
    const msgStr = String.UTF8.decode(msgBuffer);

    // Parse FIPA message
    const jsonObj = JSON.parse(msgStr);
    const performative = jsonObj.getString("performative");

    if (performative == "request") {
        return handleRequest(jsonObj);
    } else if (performative == "inform") {
        return handleInform(jsonObj);
    }

    return 1; // Error
}

function handleRequest(message: JSON.Obj): i32 {
    const content = message.getObj("content");
    const action = content?.getString("action");

    if (action == "process_data") {
        const data = content!.getString("data");
        const operation = content!.getString("operation");

        let result: string;
        if (operation == "uppercase") {
            result = data.toUpperCase();
        } else if (operation == "lowercase") {
            result = data.toLowerCase();
        } else {
            result = "Unknown operation";
        }

        // Send response back to host
        sendInformResponse(message, result);
        return 0;
    }

    return 1;
}

function sendInformResponse(originalMessage: JSON.Obj, result: string): void {
    const response = new JSON.Obj();
    response.set("performative", "inform");
    response.set("sender", originalMessage.getString("receiver"));
    response.set("receiver", originalMessage.getString("sender"));
    response.set("in_reply_to", originalMessage.getString("reply_with"));

    const content = new JSON.Obj();
    content.set("result", result);
    content.set("timestamp", getCurrentTimestamp().toString());
    response.set("content", content);

    const responseStr = response.toString();
    const responseBytes = String.UTF8.encode(responseStr);

    // Send to host via imported function
    send_message(changetype<i32>(responseBytes), responseBytes.byteLength);
}

// Imported from host
declare function send_message(ptr: i32, len: i32): void;
declare function current_timestamp(): u64;

function getCurrentTimestamp(): u64 {
    return current_timestamp();
}

Go (TinyGo)

// main.go
package main

import (
    "encoding/json"
    "unsafe"
)

//export agent_init
func agentInit() int32 {
    println("Go agent initializing...")
    return 0
}

//export agent_shutdown
func agentShutdown() {
    println("Go agent shutting down...")
}

//export handle_message
func handleMessage(msgPtr uintptr, msgLen int32) int32 {
    // Convert WASM memory to Go slice
    msgBytes := (*[1 << 30]byte)(unsafe.Pointer(msgPtr))[:msgLen:msgLen]

    var message FipaMessage
    if err := json.Unmarshal(msgBytes, &message); err != nil {
        println("Failed to parse message:", err.Error())
        return 1
    }

    switch message.Performative {
    case "request":
        return handleRequest(&message)
    case "inform":
        return handleInform(&message)
    default:
        sendNotUnderstood(&message)
        return 1
    }
}

type FipaMessage struct {
    Performative     string                 `json:"performative"`
    Sender          string                 `json:"sender"`
    Receiver        string                 `json:"receiver"`
    Content         map[string]interface{} `json:"content"`
    ConversationID  *string                `json:"conversation_id,omitempty"`
    ReplyWith       *string                `json:"reply_with,omitempty"`
    InReplyTo       *string                `json:"in_reply_to,omitempty"`
}

type ProcessRequest struct {
    Data      string `json:"data"`
    Operation string `json:"operation"`
}

type ProcessResponse struct {
    Result    string `json:"result"`
    Timestamp int64  `json:"timestamp"`
}

func handleRequest(message *FipaMessage) int32 {
    action, ok := message.Content["action"].(string)
    if !ok {
        sendFailure(message, "Missing action field")
        return 1
    }

    switch action {
    case "process_data":
        dataInterface, exists := message.Content["data"]
        if !exists {
            sendFailure(message, "Missing data field")
            return 1
        }

        dataBytes, _ := json.Marshal(dataInterface)
        var request ProcessRequest
        if err := json.Unmarshal(dataBytes, &request); err != nil {
            sendFailure(message, "Invalid data format")
            return 1
        }

        response := processData(&request)
        sendInformResponse(message, response)
        return 0

    case "get_status":
        status := getAgentStatus()
        sendInformResponse(message, status)
        return 0

    default:
        sendNotUnderstood(message)
        return 1
    }
}

func processData(request *ProcessRequest) *ProcessResponse {
    var result string

    switch request.Operation {
    case "uppercase":
        result = strings.ToUpper(request.Data)
    case "lowercase":
        result = strings.ToLower(request.Data)
    case "reverse":
        runes := []rune(request.Data)
        for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
            runes[i], runes[j] = runes[j], runes[i]
        }
        result = string(runes)
    default:
        result = "Unknown operation: " + request.Operation
    }

    return &ProcessResponse{
        Result:    result,
        Timestamp: getCurrentTimestamp(),
    }
}

//go:wasmimport env send_message
func sendMessage(ptr uintptr, len int32)

//go:wasmimport env current_timestamp
func getCurrentTimestamp() int64

func sendInformResponse(originalMessage *FipaMessage, response interface{}) {
    responseMsg := FipaMessage{
        Performative: "inform",
        Sender:       originalMessage.Receiver,
        Receiver:     originalMessage.Sender,
        Content:      map[string]interface{}{"result": response},
        InReplyTo:    originalMessage.ReplyWith,
    }

    responseBytes, _ := json.Marshal(responseMsg)
    sendMessage(uintptr(unsafe.Pointer(&responseBytes[0])), int32(len(responseBytes)))
}

func main() {}

Build Process

Rust Build

# Install WASM target
rustup target add wasm32-wasi

# Build optimized WASM
cargo build --target wasm32-wasi --release

# Optimize with wasm-opt (from binaryen)
wasm-opt -Os -o agent_optimized.wasm target/wasm32-wasi/release/my_caxton_agent.wasm

AssemblyScript Build

# Install dependencies
npm install assemblyscript @assemblyscript/wasi-shim

# Build
npx asc assembly/index.ts --target release --optimize --outFile agent.wasm

# Optimize
wasm-opt -Os -o agent_optimized.wasm agent.wasm

Go Build

# Install TinyGo
curl -L https://github.com/tinygo-org/tinygo/releases/download/v0.30.0/tinygo_0.30.0_amd64.deb -o tinygo.deb
sudo dpkg -i tinygo.deb

# Build
tinygo build -target=wasi -o agent.wasm main.go

# Optimize
wasm-opt -Os -o agent_optimized.wasm agent.wasm

Host Interface (WASI Extensions)

Caxton extends WASI with custom host functions for agent communication and system interaction.

Message Interface

// Host-provided functions (imported by agents)
#[link(wasm_import_module = "caxton")]
extern "C" {
    /// Send a FIPA message to another agent
    fn send_message(msg_ptr: *const u8, msg_len: usize) -> i32;

    /// Subscribe to messages matching a filter
    fn subscribe_messages(filter_ptr: *const u8, filter_len: usize) -> i32;

    /// Get current timestamp (microseconds since Unix epoch)
    fn current_timestamp() -> u64;

    /// Log a message (for debugging)
    fn console_log(msg_ptr: *const u8, msg_len: usize);

    /// Report an error
    fn console_error(msg_ptr: *const u8, msg_len: usize);

    /// Generate a random number
    fn random_u32() -> u32;

    /// Sleep for specified microseconds
    fn sleep_micros(micros: u64);
}

// Agent-provided functions (exported to host)
extern "C" {
    /// Called when agent is loaded
    fn agent_init() -> i32;

    /// Called when agent is being unloaded
    fn agent_shutdown();

    /// Handle incoming FIPA message
    fn handle_message(msg_ptr: *const u8, msg_len: usize) -> i32;

    /// Handle timer events
    fn handle_timer(timer_id: u32) -> i32;

    /// Provide agent capabilities description
    fn get_capabilities(caps_ptr: *mut u8, caps_len: usize) -> i32;
}

Resource Management

// Resource limit configuration
pub struct ResourceLimits {
    /// Maximum memory in bytes (0 = unlimited)
    pub max_memory_bytes: u64,

    /// Maximum CPU time per execution quantum (microseconds)
    pub max_cpu_micros: u64,

    /// Maximum execution time for single message (microseconds)
    pub max_execution_time_micros: u64,

    /// Maximum number of host function calls per execution
    pub max_host_calls: u32,

    /// Maximum outgoing messages per second
    pub max_messages_per_second: u32,

    /// Maximum memory allocations per execution
    pub max_allocations: u32,
}

impl Default for ResourceLimits {
    fn default() -> Self {
        Self {
            max_memory_bytes: 50 * 1024 * 1024, // 50MB
            max_cpu_micros: 100_000, // 100ms
            max_execution_time_micros: 1_000_000, // 1 second
            max_host_calls: 1000,
            max_messages_per_second: 100,
            max_allocations: 10000,
        }
    }
}

Capability Declaration

// Agent capabilities descriptor
#[derive(Serialize, Deserialize)]
pub struct AgentCapabilities {
    /// Agent version
    pub version: String,

    /// Supported FIPA performatives
    pub performatives: Vec<String>,

    /// Supported protocols
    pub protocols: Vec<String>,

    /// Supported ontologies
    pub ontologies: Vec<String>,

    /// Resource requirements
    pub resource_requirements: ResourceRequirements,

    /// Service descriptions
    pub services: Vec<ServiceDescription>,
}

#[derive(Serialize, Deserialize)]
pub struct ResourceRequirements {
    pub min_memory_mb: u32,
    pub preferred_memory_mb: u32,
    pub cpu_intensive: bool,
    pub network_access: bool,
    pub persistence_needed: bool,
}

#[derive(Serialize, Deserialize)]
pub struct ServiceDescription {
    pub name: String,
    pub description: String,
    pub input_schema: serde_json::Value,
    pub output_schema: serde_json::Value,
    pub estimated_latency_ms: u32,
}

Advanced Features

State Persistence

Agents can persist state between executions:

#[link(wasm_import_module = "caxton")]
extern "C" {
    /// Store persistent data
    fn store_data(key_ptr: *const u8, key_len: usize,
                  data_ptr: *const u8, data_len: usize) -> i32;

    /// Retrieve persistent data
    fn load_data(key_ptr: *const u8, key_len: usize,
                 data_ptr: *mut u8, data_len: usize) -> i32;

    /// Delete persistent data
    fn delete_data(key_ptr: *const u8, key_len: usize) -> i32;
}

// Agent implementation
use std::collections::HashMap;

struct AgentState {
    counters: HashMap<String, u64>,
    last_update: u64,
    configuration: AgentConfig,
}

impl AgentState {
    fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
        let serialized = serde_json::to_vec(self)?;
        let key = b"agent_state";

        let result = unsafe {
            store_data(
                key.as_ptr(),
                key.len(),
                serialized.as_ptr(),
                serialized.len(),
            )
        };

        if result == 0 { Ok(()) } else { Err("Failed to save state".into()) }
    }

    fn load() -> Result<AgentState, Box<dyn std::error::Error>> {
        let key = b"agent_state";
        let mut buffer = vec![0u8; 4096]; // Max state size

        let result = unsafe {
            load_data(
                key.as_ptr(),
                key.len(),
                buffer.as_mut_ptr(),
                buffer.len(),
            )
        };

        if result > 0 {
            buffer.truncate(result as usize);
            let state: AgentState = serde_json::from_slice(&buffer)?;
            Ok(state)
        } else {
            Ok(AgentState::default())
        }
    }
}

Asynchronous Operations

Handle long-running operations without blocking:

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

pub struct HostTimer {
    timer_id: u32,
    completed: bool,
}

impl Future for HostTimer {
    type Output = ();

    fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
        if self.completed {
            Poll::Ready(())
        } else {
            Poll::Pending
        }
    }
}

// Set a timer from host
#[link(wasm_import_module = "caxton")]
extern "C" {
    fn set_timer(delay_micros: u64) -> u32;
    fn cancel_timer(timer_id: u32) -> i32;
}

// Handle long-running operations
#[wasm_bindgen]
pub fn handle_message(msg_ptr: *const u8, msg_len: usize) -> i32 {
    let message = parse_message(msg_ptr, msg_len);

    match message.content.get("action").and_then(|v| v.as_str()) {
        Some("long_operation") => {
            // Start async operation
            start_long_operation(&message);

            // Return immediately - result will be sent via timer
            0
        }
        _ => {
            // Handle synchronously
            handle_sync_message(message)
        }
    }
}

fn start_long_operation(message: &FipaMessage) {
    // Set timer for 5 seconds
    let timer_id = unsafe { set_timer(5_000_000) };

    // Store operation context
    store_operation_context(timer_id, message);
}

#[wasm_bindgen]
pub fn handle_timer(timer_id: u32) -> i32 {
    if let Some(context) = load_operation_context(timer_id) {
        // Complete the operation
        let result = complete_long_operation(&context);

        // Send result back
        send_inform_response(&context.original_message, &result);

        // Cleanup
        cleanup_operation_context(timer_id);
    }

    0
}

Inter-Agent Communication

Direct communication between agents in the same runtime:

// High-performance message passing for local agents
#[link(wasm_import_module = "caxton")]
extern "C" {
    /// Send message to local agent (shared memory)
    fn send_local_message(agent_id_ptr: *const u8, agent_id_len: usize,
                         msg_ptr: *const u8, msg_len: usize) -> i32;

    /// Broadcast message to multiple agents
    fn broadcast_message(filter_ptr: *const u8, filter_len: usize,
                        msg_ptr: *const u8, msg_len: usize) -> i32;
}

// Efficient local messaging
pub fn send_to_agent(agent_id: &str, message: &FipaMessage) -> Result<(), String> {
    let agent_bytes = agent_id.as_bytes();
    let msg_bytes = serde_json::to_vec(message).map_err(|e| e.to_string())?;

    let result = unsafe {
        send_local_message(
            agent_bytes.as_ptr(),
            agent_bytes.len(),
            msg_bytes.as_ptr(),
            msg_bytes.len(),
        )
    };

    if result == 0 {
        Ok(())
    } else {
        Err(format!("Failed to send message, error code: {}", result))
    }
}

// Broadcast to multiple agents
pub fn broadcast_to_capability(capability: &str, message: &FipaMessage) -> Result<u32, String> {
    // Note: capabilities are used here for runtime filtering, not agent configuration
    // Agents register their capabilities programmatically, not through static config
    let filter = json!({
        "capabilities": [capability]
    });
    let filter_bytes = serde_json::to_vec(&filter).map_err(|e| e.to_string())?;
    let msg_bytes = serde_json::to_vec(message).map_err(|e| e.to_string())?;

    let result = unsafe {
        broadcast_message(
            filter_bytes.as_ptr(),
            filter_bytes.len(),
            msg_bytes.as_ptr(),
            msg_bytes.len(),
        )
    };

    if result >= 0 {
        Ok(result as u32)
    } else {
        Err(format!("Failed to broadcast message, error code: {}", result))
    }
}

Performance Optimization

Memory Management

Efficient memory usage in WebAssembly environments:

use std::alloc::{alloc, dealloc, Layout};
use std::ptr;

// Custom allocator for WASM
pub struct WasmAllocator {
    allocated: usize,
    max_allocated: usize,
    allocation_count: usize,
}

impl WasmAllocator {
    pub const fn new() -> Self {
        Self {
            allocated: 0,
            max_allocated: 0,
            allocation_count: 0,
        }
    }

    pub fn allocate(&mut self, size: usize, align: usize) -> *mut u8 {
        let layout = Layout::from_size_align(size, align).unwrap();
        let ptr = unsafe { alloc(layout) };

        if !ptr.is_null() {
            self.allocated += size;
            self.allocation_count += 1;
            if self.allocated > self.max_allocated {
                self.max_allocated = self.allocated;
            }
        }

        ptr
    }

    pub fn deallocate(&mut self, ptr: *mut u8, size: usize, align: usize) {
        let layout = Layout::from_size_align(size, align).unwrap();
        unsafe { dealloc(ptr, layout) };

        self.allocated -= size;
    }

    pub fn stats(&self) -> AllocationStats {
        AllocationStats {
            current_allocated: self.allocated,
            max_allocated: self.max_allocated,
            allocation_count: self.allocation_count,
        }
    }
}

// Use stack allocation for small, short-lived data
pub fn process_small_message(data: &[u8]) -> Vec<u8> {
    const STACK_SIZE: usize = 1024;

    if data.len() <= STACK_SIZE {
        // Use stack allocation
        let mut stack_buffer = [0u8; STACK_SIZE];
        let result_len = process_in_place(&mut stack_buffer[..data.len()]);
        stack_buffer[..result_len].to_vec()
    } else {
        // Fall back to heap allocation
        let mut heap_buffer = data.to_vec();
        let result_len = process_in_place(&mut heap_buffer);
        heap_buffer.truncate(result_len);
        heap_buffer
    }
}

Computation Optimization

// Optimize hot paths with manual loop unrolling
pub fn fast_data_transform(input: &[u8], output: &mut [u8]) {
    assert_eq!(input.len(), output.len());

    let len = input.len();
    let chunks = len / 4;
    let remainder = len % 4;

    // Process 4 bytes at a time (loop unrolling)
    for i in 0..chunks {
        let base = i * 4;
        output[base] = transform_byte(input[base]);
        output[base + 1] = transform_byte(input[base + 1]);
        output[base + 2] = transform_byte(input[base + 2]);
        output[base + 3] = transform_byte(input[base + 3]);
    }

    // Handle remaining bytes
    for i in 0..remainder {
        let idx = chunks * 4 + i;
        output[idx] = transform_byte(input[idx]);
    }
}

// Use lookup tables for expensive computations
static TRANSFORM_TABLE: [u8; 256] = {
    let mut table = [0u8; 256];
    let mut i = 0;
    while i < 256 {
        table[i] = ((i as f32 * 1.5) as u8).wrapping_add(42);
        i += 1;
    }
    table
};

#[inline(always)]
fn transform_byte(byte: u8) -> u8 {
    TRANSFORM_TABLE[byte as usize]
}

Binary Size Optimization

Minimize WASM binary size:

// Use smaller integer types when possible
use std::num::{NonZeroU16, NonZeroU32};

// Prefer stack-allocated arrays over Vec when size is known
const MAX_AGENTS: usize = 64;
type AgentArray = [AgentId; MAX_AGENTS];

// Use bit fields for flags
pub struct AgentFlags {
    flags: u32,
}

impl AgentFlags {
    const ACTIVE: u32 = 1 << 0;
    const PERSISTENT: u32 = 1 << 1;
    const HIGH_PRIORITY: u32 = 1 << 2;

    pub fn new() -> Self {
        Self { flags: 0 }
    }

    pub fn is_active(&self) -> bool {
        self.flags & Self::ACTIVE != 0
    }

    pub fn set_active(&mut self, active: bool) {
        if active {
            self.flags |= Self::ACTIVE;
        } else {
            self.flags &= !Self::ACTIVE;
        }
    }
}

// Use const generics to avoid runtime overhead
pub struct FixedBuffer<const N: usize> {
    data: [u8; N],
    len: usize,
}

impl<const N: usize> FixedBuffer<N> {
    pub const fn new() -> Self {
        Self {
            data: [0; N],
            len: 0,
        }
    }

    pub fn push(&mut self, byte: u8) -> Result<(), &'static str> {
        if self.len >= N {
            Err("Buffer full")
        } else {
            self.data[self.len] = byte;
            self.len += 1;
            Ok(())
        }
    }

    pub fn as_slice(&self) -> &[u8] {
        &self.data[..self.len]
    }
}

Security Considerations

Input Validation

Always validate inputs from the host and other agents:

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Deserialize)]
struct MessageContent {
    #[serde(deserialize_with = "validate_action")]
    action: String,

    #[serde(default, deserialize_with = "validate_data")]
    data: Option<serde_json::Value>,

    #[serde(default, deserialize_with = "validate_parameters")]
    parameters: HashMap<String, serde_json::Value>,
}

fn validate_action<'de, D>(deserializer: D) -> Result<String, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let action = String::deserialize(deserializer)?;

    // Whitelist allowed actions
    match action.as_str() {
        "process_data" | "get_status" | "calculate" | "transform" => Ok(action),
        _ => Err(serde::de::Error::custom(
            format!("Invalid action: {}", action)
        )),
    }
}

fn validate_data<'de, D>(deserializer: D) -> Result<Option<serde_json::Value>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let data = Option::<serde_json::Value>::deserialize(deserializer)?;

    if let Some(ref value) = data {
        // Limit size of incoming data
        let size = estimate_json_size(value);
        if size > 1024 * 1024 { // 1MB limit
            return Err(serde::de::Error::custom("Data too large"));
        }

        // Validate data structure
        validate_json_structure(value)
            .map_err(|e| serde::de::Error::custom(e))?;
    }

    Ok(data)
}

fn validate_json_structure(value: &serde_json::Value) -> Result<(), String> {
    match value {
        serde_json::Value::Object(map) => {
            if map.len() > 100 {
                return Err("Object has too many keys".to_string());
            }
            for (key, val) in map {
                if key.len() > 256 {
                    return Err("Key too long".to_string());
                }
                validate_json_structure(val)?;
            }
        }
        serde_json::Value::Array(arr) => {
            if arr.len() > 1000 {
                return Err("Array too large".to_string());
            }
            for val in arr {
                validate_json_structure(val)?;
            }
        }
        serde_json::Value::String(s) => {
            if s.len() > 10000 {
                return Err("String too long".to_string());
            }
        }
        _ => {}
    }
    Ok(())
}

fn estimate_json_size(value: &serde_json::Value) -> usize {
    match value {
        serde_json::Value::Object(map) => {
            map.iter().map(|(k, v)| k.len() + estimate_json_size(v)).sum::<usize>() + map.len() * 4
        }
        serde_json::Value::Array(arr) => {
            arr.iter().map(estimate_json_size).sum::<usize>() + arr.len() * 2
        }
        serde_json::Value::String(s) => s.len() + 2,
        _ => 16, // Approximate size for numbers, booleans, null
    }
}

Resource Monitoring

Monitor resource usage to prevent abuse:

pub struct ResourceMonitor {
    memory_usage: usize,
    cpu_time_micros: u64,
    message_count: u32,
    allocation_count: u32,
    start_time: u64,
}

impl ResourceMonitor {
    pub fn new() -> Self {
        Self {
            memory_usage: 0,
            cpu_time_micros: 0,
            message_count: 0,
            allocation_count: 0,
            start_time: unsafe { current_timestamp() },
        }
    }

    pub fn check_limits(&self, limits: &ResourceLimits) -> Result<(), ResourceError> {
        if self.memory_usage > limits.max_memory_bytes as usize {
            return Err(ResourceError::MemoryLimitExceeded);
        }

        if self.cpu_time_micros > limits.max_cpu_micros {
            return Err(ResourceError::CpuLimitExceeded);
        }

        let elapsed = unsafe { current_timestamp() } - self.start_time;
        if elapsed > limits.max_execution_time_micros {
            return Err(ResourceError::TimeoutExceeded);
        }

        if self.allocation_count > limits.max_allocations {
            return Err(ResourceError::AllocationLimitExceeded);
        }

        Ok(())
    }

    pub fn record_allocation(&mut self, size: usize) {
        self.memory_usage += size;
        self.allocation_count += 1;
    }

    pub fn record_deallocation(&mut self, size: usize) {
        self.memory_usage = self.memory_usage.saturating_sub(size);
    }

    pub fn record_message_sent(&mut self) {
        self.message_count += 1;
    }
}

#[derive(Debug)]
pub enum ResourceError {
    MemoryLimitExceeded,
    CpuLimitExceeded,
    TimeoutExceeded,
    AllocationLimitExceeded,
    MessageRateLimitExceeded,
}

Testing and Debugging

Unit Testing

Test WASM agents outside the runtime:

// tests/agent_test.rs
use serde_json::json;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_message_processing() {
        // Mock message
        let message = json!({
            "performative": "request",
            "sender": "test_client",
            "receiver": "test_agent",
            "content": {
                "action": "process_data",
                "data": {
                    "operation": "uppercase",
                    "text": "hello world"
                }
            },
            "reply_with": "test_001"
        });

        let message_bytes = serde_json::to_vec(&message).unwrap();

        // Test agent message handling
        let result = handle_message(
            message_bytes.as_ptr(),
            message_bytes.len()
        );

        assert_eq!(result, 0); // Success
    }

    #[test]
    fn test_invalid_message_format() {
        let invalid_message = b"invalid json";

        let result = handle_message(
            invalid_message.as_ptr(),
            invalid_message.len()
        );

        assert_ne!(result, 0); // Should fail
    }

    #[test]
    fn test_resource_limits() {
        let large_data = "x".repeat(1024 * 1024 * 2); // 2MB

        let message = json!({
            "performative": "request",
            "sender": "test_client",
            "receiver": "test_agent",
            "content": {
                "action": "process_data",
                "data": large_data
            }
        });

        let message_bytes = serde_json::to_vec(&message).unwrap();

        let result = handle_message(
            message_bytes.as_ptr(),
            message_bytes.len()
        );

        assert_ne!(result, 0); // Should fail due to size limit
    }
}

Integration Testing

Test agents in the full Caxton runtime:

// integration_tests/agent_deployment.rs
use caxton_client::*;
use std::fs;
use tokio::time::{timeout, Duration};

#[tokio::test]
async fn test_agent_deployment_and_communication() {
    let client = CaxtonClient::new("http://localhost:8080").await.unwrap();

    // Load WASM agent
    let wasm_bytes = fs::read("target/wasm32-wasi/release/test_agent.wasm").unwrap();

    // Deploy agent
    let deployment = client.deploy_agent(DeployAgentRequest {
        wasm_module: wasm_bytes,
        config: AgentConfig {
            name: "test_agent".to_string(),
            resources: ResourceLimits::default(),
            capabilities: vec!["data_processing".to_string()],
            ..Default::default()
        },
    }).await.unwrap();

    // Wait for agent to be ready
    tokio::time::sleep(Duration::from_millis(100)).await;

    // Send test message
    let response = client.send_message_and_wait(FipaMessage {
        performative: "request".to_string(),
        sender: "integration_test".to_string(),
        receiver: deployment.agent_id.clone(),
        content: json!({
            "action": "process_data",
            "data": {
                "operation": "uppercase",
                "text": "hello world"
            }
        }),
        reply_with: Some("test_001".to_string()),
        ..Default::default()
    }, Duration::from_secs(10)).await.unwrap();

    // Verify response
    assert_eq!(response.performative, "inform");
    assert_eq!(
        response.content["result"]["result"].as_str().unwrap(),
        "HELLO WORLD"
    );

    // Cleanup
    client.remove_agent(&deployment.agent_id).await.unwrap();
}

#[tokio::test]
async fn test_agent_resource_limits() {
    let client = CaxtonClient::new("http://localhost:8080").await.unwrap();
    let wasm_bytes = fs::read("target/wasm32-wasi/release/memory_hungry_agent.wasm").unwrap();

    // Deploy agent with strict memory limits
    let deployment = client.deploy_agent(DeployAgentRequest {
        wasm_module: wasm_bytes,
        config: AgentConfig {
            name: "memory_test_agent".to_string(),
            resources: ResourceLimits {
                max_memory_bytes: 1024 * 1024, // 1MB limit
                ..Default::default()
            },
            ..Default::default()
        },
    }).await.unwrap();

    // Send message that should exceed memory limit
    let result = timeout(
        Duration::from_secs(5),
        client.send_message_and_wait(FipaMessage {
            performative: "request".to_string(),
            sender: "integration_test".to_string(),
            receiver: deployment.agent_id.clone(),
            content: json!({
                "action": "allocate_large_buffer",
                "size": 2 * 1024 * 1024 // 2MB
            }),
            reply_with: Some("memory_test_001".to_string()),
            ..Default::default()
        }, Duration::from_secs(10))
    ).await;

    match result {
        Ok(Ok(response)) => {
            // Should receive failure message
            assert_eq!(response.performative, "failure");
            assert!(response.content["error"].as_str().unwrap().contains("memory"));
        }
        _ => panic!("Expected memory limit error"),
    }

    client.remove_agent(&deployment.agent_id).await.unwrap();
}

Debugging Tools

Debug WASM agents with logging and profiling:

// Debug utilities for agents
pub struct DebugLogger {
    enabled: bool,
    log_level: LogLevel,
}

#[derive(PartialEq, PartialOrd)]
pub enum LogLevel {
    Error = 1,
    Warn = 2,
    Info = 3,
    Debug = 4,
    Trace = 5,
}

impl DebugLogger {
    pub fn new() -> Self {
        Self {
            enabled: cfg!(debug_assertions),
            log_level: LogLevel::Info,
        }
    }

    pub fn error(&self, msg: &str) {
        if self.enabled && self.log_level >= LogLevel::Error {
            let full_msg = format!("[ERROR] {}", msg);
            unsafe {
                console_error(full_msg.as_ptr(), full_msg.len());
            }
        }
    }

    pub fn warn(&self, msg: &str) {
        if self.enabled && self.log_level >= LogLevel::Warn {
            let full_msg = format!("[WARN] {}", msg);
            unsafe {
                console_log(full_msg.as_ptr(), full_msg.len());
            }
        }
    }

    pub fn info(&self, msg: &str) {
        if self.enabled && self.log_level >= LogLevel::Info {
            let full_msg = format!("[INFO] {}", msg);
            unsafe {
                console_log(full_msg.as_ptr(), full_msg.len());
            }
        }
    }

    pub fn debug(&self, msg: &str) {
        if self.enabled && self.log_level >= LogLevel::Debug {
            let full_msg = format!("[DEBUG] {}", msg);
            unsafe {
                console_log(full_msg.as_ptr(), full_msg.len());
            }
        }
    }
}

// Performance profiler
pub struct Profiler {
    checkpoints: std::collections::HashMap<String, u64>,
    enabled: bool,
}

impl Profiler {
    pub fn new() -> Self {
        Self {
            checkpoints: std::collections::HashMap::new(),
            enabled: cfg!(debug_assertions),
        }
    }

    pub fn start(&mut self, name: &str) {
        if self.enabled {
            let timestamp = unsafe { current_timestamp() };
            self.checkpoints.insert(name.to_string(), timestamp);
        }
    }

    pub fn end(&mut self, name: &str) -> Option<u64> {
        if self.enabled {
            let end_time = unsafe { current_timestamp() };
            if let Some(start_time) = self.checkpoints.remove(name) {
                let duration = end_time - start_time;

                let log_msg = format!("Profile [{}]: {} microseconds", name, duration);
                unsafe {
                    console_log(log_msg.as_ptr(), log_msg.len());
                }

                Some(duration)
            } else {
                None
            }
        } else {
            None
        }
    }
}

// Usage in agent code
static mut LOGGER: Option<DebugLogger> = None;
static mut PROFILER: Option<Profiler> = None;

#[wasm_bindgen]
pub fn agent_init() -> i32 {
    unsafe {
        LOGGER = Some(DebugLogger::new());
        PROFILER = Some(Profiler::new());
    }

    log_info("Agent initialized successfully");
    0
}

#[wasm_bindgen]
pub fn handle_message(msg_ptr: *const u8, msg_len: usize) -> i32 {
    profile_start("message_handling");

    let result = match parse_and_handle_message(msg_ptr, msg_len) {
        Ok(_) => {
            log_debug("Message handled successfully");
            0
        }
        Err(e) => {
            log_error(&format!("Message handling failed: {}", e));
            1
        }
    };

    profile_end("message_handling");
    result
}

fn log_info(msg: &str) {
    unsafe {
        if let Some(ref logger) = LOGGER {
            logger.info(msg);
        }
    }
}

fn log_error(msg: &str) {
    unsafe {
        if let Some(ref logger) = LOGGER {
            logger.error(msg);
        }
    }
}

fn log_debug(msg: &str) {
    unsafe {
        if let Some(ref logger) = LOGGER {
            logger.debug(msg);
        }
    }
}

fn profile_start(name: &str) {
    unsafe {
        if let Some(ref mut profiler) = PROFILER {
            profiler.start(name);
        }
    }
}

fn profile_end(name: &str) {
    unsafe {
        if let Some(ref mut profiler) = PROFILER {
            profiler.end(name);
        }
    }
}

Troubleshooting

Common Issues

Agent Won’t Load

# Check WASM validity
wasm-validate agent.wasm

# Inspect WASM exports
wasm-objdump -x agent.wasm

# Check for missing imports
wasm-objdump -j Import agent.wasm

Memory Issues

// Add memory tracking
static mut ALLOCATED_BYTES: usize = 0;

#[no_mangle]
pub extern "C" fn __wbindgen_malloc(size: usize) -> *mut u8 {
    unsafe {
        ALLOCATED_BYTES += size;
        if ALLOCATED_BYTES > 50 * 1024 * 1024 { // 50MB limit
            return std::ptr::null_mut(); // Signal allocation failure
        }

        std::alloc::alloc(std::alloc::Layout::from_size_align_unchecked(size, 1))
    }
}

#[no_mangle]
pub extern "C" fn __wbindgen_free(ptr: *mut u8, size: usize) {
    unsafe {
        ALLOCATED_BYTES -= size;
        std::alloc::dealloc(ptr, std::alloc::Layout::from_size_align_unchecked(size, 1));
    }
}

Performance Problems

# Profile WASM execution
perf record -g ./caxton run-agent agent.wasm
perf report

# Analyze binary size
wasm-objdump -h agent.wasm
wasm-opt --print-stats agent.wasm

This comprehensive guide covers all aspects of WebAssembly integration in Caxton, from basic agent development to advanced optimization and debugging techniques. The sandboxed execution model provides security and isolation while maintaining high performance for agent-based systems.