Actors as WebAssembly Components
Before building your first actor, it's crucial to understand what you're actually creating: WebAssembly components that run in a sandboxed environment with controlled access to host capabilities.
This isn't just "Rust code with some macros" - you're building self-contained, portable components that interact with the Wasmind host through a well-defined interface.
What Are WebAssembly Components?
WebAssembly Components are a new standard for building composable, portable modules that can run anywhere. Think of them as:
- Sandboxed by default - Can only access capabilities explicitly granted by the host
- Language agnostic - Can be built in Rust, JavaScript, Python, or any WASM-capable language
- Interface-driven - Communicate through strictly typed interfaces, not shared memory
- Portable - Run identically across different operating systems and architectures
In Wasmind, every actor is a WebAssembly component that:
- Imports host-provided capabilities (logging, HTTP, messaging, etc.)
- Exports an actor implementation that handles messages
- Communicates through structured message passing
The Actor Interface Contract
Every Wasmind actor must implement the actor
interface defined in Wasmind's world.wit. WIT (WebAssembly Interface Types) is the interface definition language that specifies how components communicate - think of it as similar to Protocol Buffers or GraphQL schemas. Here's what that looks like:
// From world.wit - the core actor interface
resource actor {
/// Called when the actor is created
constructor(scope: scope, config: string);
/// Called for every message broadcast in the system
handle-message: func(message: message-envelope);
/// Called when the actor is shutting down
destructor: func();
}
Key insight: These three functions are the ONLY way the host can interact with your actor. Everything else happens through these entry points.
Message Envelope Structure
All communication uses this standardized envelope:
record message-envelope {
id: string, // Correlation ID for tracing
message-type: string, // Unique identifier for message type
from-actor-id: string, // Who sent this message
from-scope: scope, // Which agent sent this message
payload: list<u8>, // Serialized message data
}
Scope is a 6-character string that identifies which agent an actor belongs to. This enables Wasmind's multi-agent coordination - actors in different scopes represent different agents working on different tasks. For example, you might have one agent (scope agent1
) handling user questions while another agent (scope agent2
) processes files in the background.
Host-Provided Capabilities
The Wasmind host provides these capabilities to all actors through imports:
🗣️ Messaging
interface messaging {
broadcast: func(message-type: string, payload: list<u8>);
}
How actors communicate with each other - no direct function calls, only message passing.
Note that when called this function creates a MessageEnvelope
with the from-scope
as the actors scope and a random 6-character id for the message and broadcasts it to all actors.
📝 Logging
interface logger {
enum log-level { debug, info, warn, error }
log: func(level: log-level, message: string);
}
Structured logging that integrates with the host's logging system.
🌐 HTTP Requests
interface http {
record headers {
headers: list<tuple<string, string>>
}
variant request-error {
network-error(string),
timeout,
invalid-url(string),
builder-error(string),
}
record response {
status: u16,
headers: headers,
body: list<u8>,
}
resource request {
constructor(method: string, url: string);
header: func(key: string, value: string) -> request;
headers: func(headers: headers) -> request;
body: func(body: list<u8>) -> request;
timeout: func(seconds: u32) -> request;
retry: func(max-attempts: u32, base-delay-ms: u64) -> request;
retry-on-status-codes: func(codes: list<u16>) -> request;
send: func() -> result<response, request-error>;
}
}
Full HTTP client with retry logic, timeouts, error handling, and configurable retry status codes.
⚡ Command Execution
interface command {
variant exit-status {
exited(u8),
signaled(u8),
failed-to-start(string),
timeout-expired,
}
record command-output {
stdout: list<u8>,
stderr: list<u8>,
status: exit-status,
stdout-truncated: bool,
stderr-truncated: bool,
}
resource cmd {
constructor(command: string);
args: func(args: list<string>) -> cmd;
current-dir: func(dir: string) -> cmd;
timeout: func(seconds: u32) -> cmd;
max-output-bytes: func(bytes: u32) -> cmd;
env: func(key: string, value: string) -> cmd;
env-clear: func() -> cmd;
run: func() -> result<command-output, string>;
}
}
Execute system commands with fine-grained control over execution environment, output limits, and environment variables.
🏗️ Agent Management
interface agent {
spawn-agent: func(actor-ids: list<string>, agent-name: string) -> result<scope, string>;
get-parent-scope: func() -> option<scope>;
get-parent-scope-of: func(scope: scope) -> option<scope>;
}
Spawn new agents and navigate the agent hierarchy. Query parent relationships for any scope.
💻 Host Information
interface host-info {
record os-info {
os: string,
arch: string,
}
get-host-working-directory: func() -> string;
get-host-os-info: func() -> os-info;
}
Access to real host environment information including OS type and architecture.
The Complete World Definition
The actor-world
brings it all together:
world actor-world {
// What the host provides to actors
import messaging;
import command;
import http;
import logger;
import agent;
import host-info;
// What actors must provide to the host
export actor;
}
This defines the complete contract: actors can use any imported capability and must export an actor
implementation.
The Convenience of Macros
The wasmind_actor_utils
crate provides optional macros that make building actors much simpler:
#![allow(unused)] fn main() { // With convenience macros - clean and simple #[derive(wasmind_actor_utils::actors::macros::Actor)] pub struct MyActor { scope: String, } impl GeneratedActorTrait for MyActor { fn new(scope: String, config: String) -> Self { Self { scope } } fn handle_message(&mut self, message: MessageEnvelope) { // Your actual logic here } fn destructor(&mut self) {} } }
These macros handle all the WebAssembly component plumbing for you - connecting your Rust code to the WebAssembly interface, managing the component lifecycle, and handling message serialization.
Important: These macros are conveniences, not requirements. You could implement the WebAssembly component interface directly if needed, but the macros make development much more pleasant by letting you focus on your actor's logic rather than low-level details. If you're curious about what the macros do or want to implement the interface yourself, check out the macro source code.
Security and Sandboxing
This WebAssembly component architecture enables powerful security capabilities:
Current State:
- Memory isolation - actors can't access each other's memory
- Interface-controlled access - actors can only use explicitly imported capabilities
- Message-based communication - no direct function calls between actors
Planned Security Features:
- File system restrictions - fine-grained control over which files/directories actors can access
- Command execution limits - restrict which system commands can be executed
- Resource limits - CPU, memory, and execution time controls
- Network access controls - granular permissions for HTTP requests
Current Capabilities: Actors currently have access to:
- Full HTTP client functionality
- System command execution with environment control
- Host file system (restrictions planned)
- Structured logging
- Message broadcasting
- Agent spawning and coordination
The sandboxing foundation is in place through WebAssembly's memory isolation and interface-based capability system, with enhanced restrictions coming in future releases.
Key Takeaways
Understanding actors as WebAssembly components explains:
- Why message passing is required - actors are isolated and can't share memory
- Where capabilities come from - the host provides them through imports
- How sandboxing works - actors only have access to explicitly granted capabilities
- Why the interface is strictly typed - WIT ensures type safety across the component boundary
- How portability is achieved - the same component runs identically anywhere
When you build an actor, you're creating a portable, sandboxed component that can run in any Wasmind host environment while only accessing the capabilities it actually needs.
Next Steps
Now that you understand the WebAssembly foundation, you're ready to build your first actor with full knowledge of what's happening under the hood.
The macros and utilities exist to make this easy, but you now understand they're conveniences built on top of a robust, standardized component interface.