Omni

Developers

SDK Reference

Build extensions for Omni using the Rust SDK. Compile to WebAssembly and publish to the marketplace.

Overview

Omni extensions are written in Rust and compiled to WebAssembly (WASM). They run in isolated sandboxes powered by Wasmtime with capability-based permissions — extensions can only access resources you explicitly grant them.

The omni-sdk crate provides typed clients for every host function, a macro for WASM entry-point generation, and re-exports of serde / serde_json so you can focus on your extension's logic.

Language

Rust

Compile Target

wasm32-wasip1

Runtime

Wasmtime (WASI P1)

Quick Start

Create a new Rust library project and add the Omni SDK:

terminal

cargo new --lib my-extension

cd my-extension

 

# Add the SDK dependency

cargo add omni-sdk

 

# Add serde (re-exported by SDK, but needed for derive macros)

cargo add serde --features derive

cargo add serde_json

Set the crate type to cdylib in your Cargo.toml:

Cargo.toml

[lib]

crate-type = ["cdylib"]

Write your first extension in src/lib.rs:

src/lib.rs

use omni_sdk::prelude::*;

 

#[derive(Default)]

struct MyExtension;

 

impl Extension for MyExtension {

fn handle_tool(

&mut self,

ctx: &Context,

tool_name: &str,

params: serde_json::Value,

) -> ToolResult {

match tool_name {

"hello" => {

let name = params["name"]

.as_str()

.unwrap_or("world");

Ok(serde_json::json!({

"message": format!("Hello, {}!", name)

}))

}

_ => Err(SdkError::UnknownTool(tool_name.into())),

}

}

}

 

// Generate the WASM entry point

omni_sdk::omni_main!(MyExtension);

Build for the WASM target:

terminal

# One-time setup

rustup target add wasm32-wasip1

 

# Build

cargo build --target wasm32-wasip1 --release

 

# Output: target/wasm32-wasip1/release/my_extension.wasm

Manifest Format

Every extension needs an omni-extension.toml manifest file that describes the extension, its runtime constraints, permission requirements, and tool definitions.

omni-extension.toml

[extension]

id = "com.example.weather"

name = "Weather Tool"

version = "1.0.0"

author = "Your Name <you@example.com>"

description = "Get current weather data for any city."

license = "MIT"

categories = ["weather", "utilities"]

 

[runtime]

entrypoint = "weather.wasm"

max_memory_mb = 64 # default: 64 MB

max_cpu_ms_per_call = 5000 # default: 5 seconds

max_concurrent_calls = 4 # default: 4

 

[[permissions]]

capability = "network.http"

reason = "Fetch weather data from OpenWeatherMap API"

required = true

 

[permissions.scope]

domains = ["api.openweathermap.org"]

methods = ["GET"]

 

[[permissions]]

capability = "storage.persistent"

reason = "Cache weather responses"

required = false

 

[[tools]]

name = "get_weather"

description = "Get current weather for a city"

 

[tools.parameters]

type = "object"

required = ["city"]

 

[tools.parameters.properties.city]

type = "string"

description = "City name (e.g. London, Tokyo)"

Manifest Sections

[extension]id, name, version, author, description

Required metadata. ID uses reverse-domain format (min 5 chars, must contain a dot). Version must be valid semver. Optional fields: license, homepage, repository, icon, categories, min_omni_version.

[runtime]entrypoint, max_memory_mb, max_cpu_ms_per_call, max_concurrent_calls

WASM sandbox configuration. entrypoint is the path to the .wasm file (required). Memory default is 64 MB, CPU timeout default is 5000 ms, concurrency default is 4. All limits must be greater than zero.

[[permissions]]capability, scope, reason, required

Array of permission declarations. Each entry names a capability (e.g. network.http), explains why it's needed, and optionally constrains scope (allowed domains, paths, etc.). required defaults to true.

[[tools]]name, description, parameters

Array of tool definitions exposed to the LLM. parameters is a JSON Schema object describing the tool's input. The LLM uses name and description to decide when to invoke the tool.

[config]fields.{name}.type, label, help, sensitive, required, default, options

Optional configuration schema. Defines fields the user can set through the UI (e.g. API keys, preferences). Sensitive fields are hidden in the UI. Extensions read config values via ctx.config().get(key).

[hooks]on_install, on_message, on_schedule

Optional lifecycle hooks. on_install and on_message are booleans. on_schedule accepts a cron expression (e.g. "0 */6 * * *").

Extension ID Rules

01

Must use reverse-domain format (e.g. com.example.my-tool)

02

Minimum 5 characters, must contain at least one dot

03

No leading, trailing, or consecutive dots

04

No double-dot sequences (..)

Extension Trait & Entry Point

Every extension implements the Extension trait and uses the omni_main! macro to generate the WASM entry point. Your struct must implement Default since the runtime creates the instance on first tool call.

Extension trait

pub trait Extension {

fn handle_tool(

&mut self,

ctx: &Context,

tool_name: &str,

params: serde_json::Value,

) -> ToolResult;

}

 

// ToolResult = Result<serde_json::Value, SdkError>

The handle_tool method receives the tool name as a string and parameters as a JSON value. Match on the tool name to dispatch to the correct handler. Return a serde_json::Value on success — the runtime serializes it back to the host.

multi-tool extension

use omni_sdk::prelude::*;

 

#[derive(Default)]

struct FileTools;

 

impl Extension for FileTools {

fn handle_tool(

&mut self,

ctx: &Context,

tool_name: &str,

params: serde_json::Value,

) -> ToolResult {

match tool_name {

"read_file" => self.read_file(ctx, params),

"write_file" => self.write_file(ctx, params),

"list_dir" => self.list_dir(ctx, params),

_ => Err(SdkError::UnknownTool(tool_name.into())),

}

}

}

 

impl FileTools {

fn read_file(&self, ctx: &Context, params: serde_json::Value) -> ToolResult {

let path = params["path"].as_str()

.ok_or_else(|| SdkError::Other("missing path".into()))?;

let content = ctx.fs().read_string(path)?;

Ok(serde_json::json!({ "content": content }))

}

// ... other handlers

}

 

omni_sdk::omni_main!(FileTools);

omni_main! Macro

The omni_main! macro generates a #[no_mangle] pub extern "C" fn handle_tool function that the Wasmtime runtime calls. It creates a static extension instance via Default::default() on first invocation, reads the tool name and parameters from WASM linear memory, calls your handle_tool implementation, and writes the result back to memory using a packed pointer encoding.

Context API

Every tool invocation receives a Context reference. The Context provides typed clients for every host function. Each client method that accesses a protected resource requires the corresponding permission in your manifest.

ctx.http()network.http

HttpClientMake HTTP requests (GET, POST, PUT, DELETE). Returns HttpResponse with status and body.

ctx.fs()filesystem.read / filesystem.write

FsClientRead and write files on the host filesystem. Supports both raw bytes and string content.

ctx.process()process.spawn

ProcessClientExecute commands and capture stdout/stderr. Returns ProcessOutput with exit code.

ctx.storage()none

StorageClientExtension-scoped persistent key-value store. Always available, no permission needed.

ctx.llm()ai.inference

LlmClientSend prompts to the user's configured LLM provider. Returns response text.

ctx.channels()channel.send

ChannelClientSend messages through connected channels (Discord, Telegram, Slack, etc.).

ctx.config()none

ConfigClientRead extension config values set by the user. Always available, no permission needed.

ctx.log() / .info() / .warn() / .error() / .debug()none

()Structured logging at four levels (Error, Warn, Info, Debug). Always available.

HttpClient

HTTP examples

// Simple GET request

let resp = ctx.http().get("https://api.example.com/data")?;

let body = resp.text()?; // String

let data: MyType = resp.json()?; // Deserialize

 

// POST with JSON body and custom headers

let resp = ctx.http()

.post("https://api.example.com/submit")

.header("Authorization", "Bearer token123")

.json(&my_data)?

.send()?;

 

// PUT and DELETE also available

let resp = ctx.http().delete("https://api.example.com/item/42")?;

FsClient

filesystem examples

// Read file as string

let content = ctx.fs().read_string("/path/to/file.txt")?;

 

// Read file as raw bytes

let bytes = ctx.fs().read("/path/to/image.png")?;

 

// Write string content

ctx.fs().write_string("/path/to/output.md", &markdown)?;

 

// Write raw bytes

ctx.fs().write("/path/to/data.bin", &bytes)?;

ProcessClient

process example

let output = ctx.process().exec("git", &["log", "--oneline", "-5"])?;

 

if output.exit_code == 0 {

ctx.info(&format!("git output: {}}", output.stdout));

} else {

ctx.error(&format!("git failed: {}}", output.stderr));

}

LlmClient & ChannelClient

LLM and channel examples

// LLM inference — 0 means use provider's default max tokens

let response = ctx.llm().request("Summarize this text: ...", 0)?;

 

// LLM with explicit token limit

let response = ctx.llm().request("Translate to French: hello", 200)?;

 

// Send a message through a connected channel

// Channel ID uses compound format: type:instance_id

let result = ctx.channels().send(

"discord:production", // channel_id

"#general", // recipient

"Build deployed!", // message text

)?;

StorageClient & ConfigClient

storage and config examples

// Persistent key-value storage (extension-scoped)

ctx.storage().set("last_run", "2025-01-15T10:30:00Z")?;

let value = ctx.storage().get("last_run")?; // Option<String>

ctx.storage().delete("old_key")?;

 

// Read user-set configuration values

let api_key = ctx.config().get("api_key")?; // Option<String>

let theme = ctx.config().get_or("theme", "dark")?; // String (with default)

Host Functions

Host functions are provided by the Omni runtime and bound into the WASM sandbox under the omni import module. The SDK wraps these in typed clients — you should use the Context API above rather than calling FFI functions directly.

log

Log a message at Error/Warn/Info/Debug level. Always available.

none
storage_get

Read a value from extension-scoped persistent storage.

none
storage_set

Write a value to extension-scoped persistent storage.

none
http_request

Make an HTTP request (GET/POST/PUT/DELETE/PATCH/HEAD). 30s timeout, 5 MB response limit.

network.http
fs_read

Read a file from the host filesystem. 10 MB file size limit.

filesystem.read
fs_write

Write data to a file on the host filesystem. Creates parent directories if needed.

filesystem.write
process_spawn

Execute a command with arguments and capture stdout/stderr. Output capped at 50 KB each.

process.spawn
llm_request

Send a prompt to the user's configured LLM provider and receive the response.

ai.inference
channel_send

Send a text message through a connected channel plugin.

channel.send
config_get

Read an extension configuration value set by the user. No permission required.

none

Return Code Convention

Permission-gated host functions use a consistent return code scheme. The SDK clients handle these automatically and return typed errors:

-1

Permission denied

-2

Needs user prompt

-3

Operation failed

-4

No callback set

Permissions

Omni uses a deny-by-default capability system. Extensions must declare every capability they need in their manifest, and users must grant each one before it takes effect. Scope constraints let you limit access to specific domains, paths, or executables.

Available Capabilities

network.http

HTTP requests to allowed domains

domains, methods, ports
network.websocket

WebSocket connections

domains
filesystem.read

Read files from the host filesystem

paths, extensions, max_size
filesystem.write

Write files to the host filesystem

paths, extensions, max_size
process.spawn

Execute commands on the host

executables, allowed_args, denied_args, max_concurrent
storage.persistent

Persistent key-value storage

max_bytes
ai.inference

LLM inference requests

max_tokens, rate_limit
channel.send

Send messages through channels

channels, rate_limit
browser.scrape

Scrape web content via browser

domains, max_pages
search.web

Web search queries

providers, rate_limit
messaging.sms

Send SMS messages

recipients, rate_limit
messaging.email

Send email messages

recipients, rate_limit
messaging.chat

Send chat messages

recipients, rate_limit
clipboard.read

Read clipboard contents

none
clipboard.write

Write to clipboard

none
system.notifications

Show system notifications

none
system.scheduling

Schedule recurring/one-time tasks (cron)

none
device.camera

Access device camera

none
device.microphone

Access device microphone

none
device.location

Access device location

none

Scope Examples

omni-extension.toml

# HTTP: restrict to specific domains and methods

[[permissions]]

capability = "network.http"

reason = "Fetch data from GitHub API"

[permissions.scope]

domains = ["api.github.com", "*.githubusercontent.com"]

methods = ["GET"]

 

# Filesystem: restrict to specific paths and file types

[[permissions]]

capability = "filesystem.read"

reason = "Read markdown files from Documents"

[permissions.scope]

paths = ["~/Documents"]

extensions = [".md", ".txt"]

max_size = 10000000 # 10 MB

 

# Process: whitelist specific executables

[[permissions]]

capability = "process.spawn"

reason = "Run git commands"

[permissions.scope]

executables = ["git"]

denied_args = ["push.*--force"] # regex patterns

 

# AI: limit token usage and rate

[[permissions]]

capability = "ai.inference"

reason = "Summarize file contents"

[permissions.scope]

max_tokens = 4000

rate_limit = 60 # per minute

 

# Channel: restrict to specific instances

[[permissions]]

capability = "channel.send"

reason = "Send notifications to Discord"

[permissions.scope]

channels = ["discord:production"]

rate_limit = 10

Error Handling

The SDK provides an SdkError enum for all error conditions. Tool functions return ToolResult which is Result<serde_json::Value, SdkError>.

omni_sdk::SdkError

pub enum SdkError {

UnknownTool(String), // Tool name not recognized

Serde(String), // JSON serialization error

PermissionDenied(String), // Capability not granted

HttpError(String), // HTTP request failed

StorageError(String), // Storage operation failed

FsError(String), // Filesystem operation failed

ProcessError(String), // Process execution failed

LlmError(String), // LLM inference failed

ChannelError(String), // Channel send failed

NotAvailable(String), // No callback configured

Other(String), // Generic error

}

SdkError implements Display with descriptive messages for each variant. Use the ? operator to propagate errors naturally:

error handling patterns

// The ? operator works naturally with ToolResult

fn my_tool(ctx: &Context, params: serde_json::Value) -> ToolResult {

let url = params["url"].as_str()

.ok_or_else(|| SdkError::Other("missing url parameter".into()))?;

 

let resp = ctx.http().get(url)?; // HttpError or PermissionDenied

let text = resp.text()?; // Serde error

 

Ok(serde_json::json!({ "content": text }))

}

Building & Testing

build commands

# One-time: add the WASM target

rustup target add wasm32-wasip1

 

# Debug build

cargo build --target wasm32-wasip1

 

# Release build (use for publishing)

cargo build --target wasm32-wasip1 --release

 

# Optional: optimize with wasm-opt (binaryen)

wasm-opt -Oz target/wasm32-wasip1/release/my_extension.wasm -o my_extension.wasm

Testing Locally

01

Build the WASM binary in release mode

02

Place the .wasm file and omni-extension.toml in a directory together

03

Copy the directory into Omni's extensions folder (see paths below)

04

Restart Omni — extension discovery runs on startup

05

Review and grant permissions when prompted, then test your tools

extensions directory

# Place extensions under the "user" subdirectory:

 

# Windows: %APPDATA%/com.omni.agent/extensions/user/

# macOS: ~/Library/Application Support/com.omni.agent/extensions/user/

# Linux: ~/.local/share/com.omni.agent/extensions/user/

 

# Directory structure:

# extensions/user/com.example.weather/

# ├── omni-extension.toml

# └── weather.wasm

Sandbox Limits

Extensions run in a Wasmtime sandbox with enforced resource limits. If an extension exceeds its CPU timeout, Wasmtime triggers an epoch interrupt and the call returns a timeout error. Memory limits are enforced via StoreLimits. Concurrency is controlled by a per-extension semaphore.

Memory

64 MB

max_memory_mb

CPU Timeout

5,000 ms

max_cpu_ms_per_call

Concurrency

4 parallel

max_concurrent_calls

Prelude

Import everything you need with a single use omni_sdk::prelude::* statement. The prelude re-exports all types, clients, and serde / serde_json:

Contextstruct
Extensiontrait
ToolResulttype alias
SdkErrorenum
LogLevelenum
HttpClientstruct
HttpResponsestruct
RequestBuilderstruct
FsClientstruct
ProcessClientstruct
ProcessOutputstruct
StorageClientstruct
LlmClientstruct
ChannelClientstruct
ConfigClientstruct
Serializeserde
Deserializeserde
serde_jsonmodule

Complete Example

A complete extension that fetches weather data, uses configuration for the API key, caches results in storage, and logs its activity:

omni-extension.toml

[extension]

id = "com.example.weather"

name = "Weather Tool"

version = "1.0.0"

author = "Jane Doe <jane@example.com>"

description = "Get current weather data for any city."

license = "MIT"

categories = ["weather", "utilities"]

 

[runtime]

entrypoint = "weather.wasm"

 

[[permissions]]

capability = "network.http"

reason = "Fetch weather data from OpenWeatherMap"

[permissions.scope]

domains = ["api.openweathermap.org"]

methods = ["GET"]

 

[config.fields.api_key]

type = "string"

label = "OpenWeatherMap API Key"

sensitive = true

required = true

 

[[tools]]

name = "get_weather"

description = "Get current weather for a city"

[tools.parameters]

type = "object"

required = ["city"]

[tools.parameters.properties.city]

type = "string"

description = "City name (e.g. London, Tokyo)"

src/lib.rs

use omni_sdk::prelude::*;

 

#[derive(Deserialize)]

struct WeatherResponse {

main: MainData,

weather: Vec<WeatherInfo>,

name: String,

}

 

#[derive(Deserialize)]

struct MainData { temp: f64, humidity: u32 }

 

#[derive(Deserialize)]

struct WeatherInfo { description: String }

 

#[derive(Default)]

struct WeatherExtension;

 

impl Extension for WeatherExtension {

fn handle_tool(

&mut self,

ctx: &Context,

tool_name: &str,

params: serde_json::Value,

) -> ToolResult {

match tool_name {

"get_weather" => {

let city = params["city"].as_str()

.ok_or_else(|| SdkError::Other("missing city".into()))?;

 

// Read API key from user configuration

let api_key = ctx.config().get("api_key")?

.ok_or_else(|| SdkError::Other("api_key not configured".into()))?;

 

// Check cache first

let cache_key = format!("weather_{}}", city.to_lowercase());

if let Some(cached) = ctx.storage().get(&cache_key)? {

ctx.debug("returning cached weather data");

let v: serde_json::Value = serde_json::from_str(&cached)

.map_err(|e| SdkError::Serde(e.to_string()))?;

return Ok(v);

}

 

// Fetch from API

ctx.info(&format!("fetching weather for {}}", city));

let url = format!(

"https://api.openweathermap.org/data/2.5/weather?q={}&appid={}&units=metric",

city, api_key

);

let resp = ctx.http().get(&url)?;

let weather: WeatherResponse = resp.json()?;

 

let result = serde_json::json!({

"city": weather.name,

"temperature": weather.main.temp,

"humidity": weather.main.humidity,

"description": weather.weather.first()

.map(|w| w.description.as_str()).unwrap_or("unknown"),

});

 

// Cache the result

let _ = ctx.storage().set(

&cache_key,

&serde_json::to_string(&result).unwrap_or_default(),

);

 

Ok(result)

}

_ => Err(SdkError::UnknownTool(tool_name.into())),

}

}

}

 

omni_sdk::omni_main!(WeatherExtension);

SDK Reference — Build WASM AI Extensions | Omni AI Agent Builder