Skip to main content

GarraIA WASM Plugin SDK

Build plugins for GarraIA using Rust and WebAssembly.

Overview​

GarraIA plugins are compiled to WASM (WebAssembly) and run inside a sandboxed Wasmtime runtime. Each plugin gets its own isolated execution environment with capability-based permissions.

Quick Start​

1. Create a new plugin project​

cargo new --lib my-garraia-plugin
cd my-garraia-plugin

2. Configure Cargo.toml​

[package]
name = "my-garraia-plugin"
version = "0.1.0"
edition = "2024"

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

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"

3. Implement the plugin​

use serde::{Deserialize, Serialize};

#[derive(Serialize)]
struct Output {
success: bool,
data: String,
}

fn main() {
// Read input from stdin
let mut input = String::new();
std::io::stdin().read_line(&mut input).unwrap();

// Process
let result = Output {
success: true,
data: format!("processed: {}", input.trim()),
};

// Write output to stdout
println!("{}", serde_json::to_string(&result).unwrap());
}

4. Compile to WASM​

# Add the WASI target
rustup target add wasm32-wasip1

# Build
cargo build --target wasm32-wasip1 --release

5. Create the plugin manifest​

Create plugin.toml in your plugin directory:

[plugin]
name = "my-plugin"
version = "0.1.0"
description = "My custom GarraIA plugin"

[permissions]
filesystem = false
network = []
env_vars = []

[limits]
timeout_secs = 30
max_memory_mb = 64
max_output_bytes = 1048576

6. Install the plugin​

Copy your plugin directory to ~/.garraia/plugins/my-plugin/:

~/.garraia/plugins/my-plugin/
plugin.toml
my-plugin.wasm (or plugin.wasm)

Plugin Manifest Format​

The plugin.toml file describes your plugin:

[plugin]
name = "plugin-name" # Required: alphanumeric + hyphens
version = "1.0.0" # Required: semver
description = "What it does" # Required

[permissions]
filesystem = false # Enable filesystem access
filesystem_read_paths = ["./data"] # Read-only directories (relative to plugin root)
filesystem_write_paths = ["./output"] # Read-write directories
network = ["api.example.com"] # Allowlisted network domains
env_vars = ["MY_API_KEY"] # Environment variables to pass through

[limits]
timeout_secs = 30 # Max execution time (default: 30)
max_memory_mb = 64 # Max memory usage in MiB (default: 64)
max_output_bytes = 1048576 # Max stdout/stderr bytes (default: 1MB)

Available Host Functions​

Plugins can call these host functions to interact with GarraIA:

FunctionDescription
send_messageSend a chat message to a channel/session
read_fileRead a file from the host filesystem (scoped)
http_requestMake an HTTP request (allowlisted domains only)
logWrite a log entry to the host's tracing system
get_configRead plugin configuration values
set_stateStore persistent plugin state
get_stateRetrieve previously stored plugin state

send_message​

Send a message to a chat session or channel.

{
"target": "session-id-or-channel",
"content": "Hello from my plugin!",
"metadata": { "plugin": "my-plugin" }
}

read_file​

Read a file within the plugin's allowed filesystem scope.

{
"path": "data/config.json",
"max_bytes": 1048576
}

Returns:

{
"content": "file contents...",
"size": 1234,
"truncated": false
}

http_request​

Make an HTTP request to an allowlisted domain.

{
"method": "POST",
"url": "https://api.example.com/translate",
"headers": { "Content-Type": "application/json" },
"body": "{\"text\":\"hello\",\"target\":\"es\"}",
"timeout_secs": 10
}

log​

Write a structured log entry.

{
"level": 2,
"message": "Processing completed successfully"
}

Levels: 0=Trace, 1=Debug, 2=Info, 3=Warn, 4=Error

Example Plugins​

Translator Plugin​

use serde::{Deserialize, Serialize};
use std::io::Read;

#[derive(Deserialize)]
struct TranslateInput {
text: String,
target_lang: String,
}

#[derive(Serialize)]
struct TranslateOutput {
success: bool,
translated: String,
source_lang: String,
target_lang: String,
}

fn main() {
let mut input = String::new();
std::io::stdin().read_to_string(&mut input).unwrap();

let req: TranslateInput = serde_json::from_str(&input).unwrap();

// In a real plugin, call a translation API via http_request
let output = TranslateOutput {
success: true,
translated: format!("[{}] {}", req.target_lang, req.text),
source_lang: "auto".into(),
target_lang: req.target_lang,
};

println!("{}", serde_json::to_string(&output).unwrap());
}

Code Formatter Plugin​

use serde::{Deserialize, Serialize};
use std::io::Read;

#[derive(Deserialize)]
struct FormatInput {
code: String,
language: String,
}

#[derive(Serialize)]
struct FormatOutput {
success: bool,
formatted: String,
changes: usize,
}

fn main() {
let mut input = String::new();
std::io::stdin().read_to_string(&mut input).unwrap();

let req: FormatInput = serde_json::from_str(&input).unwrap();

// Simple indentation fixer as example
let formatted = req.code
.lines()
.map(|line| line.trim_start().to_string())
.collect::<Vec<_>>()
.join("\n");

let changes = if formatted != req.code { 1 } else { 0 };

let output = FormatOutput {
success: true,
formatted,
changes,
};

println!("{}", serde_json::to_string(&output).unwrap());
}

Summarizer Plugin​

use serde::{Deserialize, Serialize};
use std::io::Read;

#[derive(Deserialize)]
struct SummarizeInput {
text: String,
max_sentences: Option<usize>,
}

#[derive(Serialize)]
struct SummarizeOutput {
success: bool,
summary: String,
original_length: usize,
summary_length: usize,
}

fn main() {
let mut input = String::new();
std::io::stdin().read_to_string(&mut input).unwrap();

let req: SummarizeInput = serde_json::from_str(&input).unwrap();
let max = req.max_sentences.unwrap_or(3);

// Simple extractive summary: take first N sentences
let sentences: Vec<&str> = req.text.split(". ").collect();
let summary = sentences.iter()
.take(max)
.copied()
.collect::<Vec<_>>()
.join(". ");

let output = SummarizeOutput {
success: true,
original_length: req.text.len(),
summary_length: summary.len(),
summary,
};

println!("{}", serde_json::to_string(&output).unwrap());
}

Security Model​

Plugins run in a sandboxed environment with these protections:

  • Memory isolation: Each plugin has its own WASM linear memory, capped by max_memory_mb
  • Execution timeout: Enforced via Wasmtime epoch interruption
  • Filesystem scoping: Only explicitly allowed directories are accessible
  • Network filtering: DNS resolution checks prevent SSRF (private IPs blocked)
  • Output limiting: stdout/stderr are capped to prevent memory exhaustion

Publishing​

Package your plugin​

# Directory structure
my-plugin/
plugin.toml
my-plugin.wasm
README.md (optional)

Using garraia plugin publish​

# Build and package
cargo build --target wasm32-wasip1 --release
cp target/wasm32-wasip1/release/my_plugin.wasm ./my-plugin.wasm

# Publish (future: plugin registry)
garraia plugin publish ./my-plugin/

Template: Cargo Generate​

# Future: cargo-generate template
cargo generate --git https://github.com/michelbr84/garraia-plugin-template

API Reference​

Plugin Installation API​

POST /api/plugins/install
Content-Type: application/json

{
"url": "https://example.com/my-plugin-manifest.json"
}

List Installed Plugins​

GET /api/plugins

Plugin Details​

GET /api/plugins/{id}

Toggle Plugin​

POST /api/plugins/{id}/toggle
Content-Type: application/json

{ "enabled": true }

Uninstall Plugin​

DELETE /api/plugins/{id}