Unicity AOS Developer GuideCapsule anatomy

Capsule anatomy

A capsule is an installable WebAssembly component plus a declarative manifest. It is not a daemon plugin and it is not a Rust library loaded into the kernel. The runtime instantiates it inside a sandbox and exposes only the WIT imports and capabilities approved at installation.

Create the package

First-party capsules live at capsules/capsule-<name> in the AOS CE workspace. A minimal layout is:

capsules/capsule-greeter/
├── .cargo/config.toml
├── Cargo.toml
├── Capsule.toml
├── README.md
└── src/lib.rs

Use the workspace SDK and compile as a cdylib.

[package]
name = "astrid-capsule-greeter"
version = "0.1.0"
edition = "2024"
license = "MIT OR Apache-2.0"

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

[dependencies]
astrid-sdk = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }

The published package name can retain the astrid-capsule-* compatibility identity while its description and documentation correctly place it in AOS.

The checked-in target configuration makes a plain capsule build produce the component input expected by the packaging tool:

[build]
target = "wasm32-unknown-unknown"

[target.wasm32-unknown-unknown]
rustflags = ["--cfg=getrandom_backend=\"custom\""]

That custom random backend routes entropy through the audited astrid:sys host import. Do not switch the capsule to WASI to make an undeclared host call work.

Implement one handler

The SDK macros generate the component export surface. Keep the handler small and move deterministic logic into ordinary Rust functions that host tests can call.

#![deny(unsafe_code)]

use astrid_sdk::prelude::*;

#[derive(Default)]
pub struct Greeter;

#[capsule]
impl Greeter {
    #[astrid::interceptor("handle_greet")]
    pub fn handle_greet(&self, payload: serde_json::Value) -> Result<(), SysError> {
        let name = payload.get("name").and_then(serde_json::Value::as_str).unwrap_or("agent");
        ipc::publish_json(
            "greeter.v1.response.greet",
            &serde_json::json!({ "message": format!("Hello, {name}") }),
        )
    }
}

Never trust a caller identity supplied inside payload. The runtime-stamped envelope is the authority for the current invocation. Validate size, required fields, enum values, and paths before touching a host import.

Declare the same contract

Capsule.toml is the runtime’s installation boundary. The subscribe handler must match the exported macro name, and every response topic must be allowed by [publish].

[package]
name = "astrid-capsule-greeter"
version = "0.1.0"
description = "Greeting example for Unicity AOS"
astrid-version = ">=0.9.4"

[[component]]
id = "greeter"
file = "astrid_capsule_greeter.wasm"
type = "executable"

[subscribe]
"greeter.v1.request.greet" = { wit = "@example/greeter/greet-request", handler = "handle_greet" }

[publish]
"greeter.v1.response.greet" = { wit = "@example/greeter/greet-response" }

Only one trailing wildcard is accepted for normal subtree subscriptions. Prefer concrete publish topics when the capsule can enumerate them. A handler return value is not a bus response; publish the response explicitly.

First validation loop

cd capsules/capsule-greeter
cargo fmt -- --check
cargo build
cd ../..
cargo test --locked -p astrid-capsule-greeter

Plain cargo build verifies the WebAssembly compilation configured by the capsule. Run host tests from the workspace root so the member’s WebAssembly target configuration does not try to execute a browserless .wasm test binary. The build does not produce an installable .capsule; packaging is a separate step covered in Build and package.

Run it.

One daemon. Every frontend is an uplink; every ability is a capsule.

$ curl -fsSL https://aos.unicity.ai/install.sh | sh