capsule-development
How to build, configure, and install Astrid capsules from scratch
Datos de origen
- Repositorio
- unicity-aos/capsule-system
- Última actividad en el origen
- 29 de mayo de 2026 a las 14:00
- Idioma detectado de SKILL.md
- inglés
- Estrellas
- 1
- Forks
- 0
Opciones de instalación
De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.
Revisa los archivos de origen
Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.
Mostrando SKILL.md
SKILL.md
Instrucciones de origen · Vista previa de solo lectura- name
- Capsule Development
- description
- How to build, configure, and install Astrid capsules from scratch
# Capsule Development
An Astrid capsule is a WebAssembly (WASM) module compiled from Rust that runs inside the Astrid kernel sandbox. Capsules communicate exclusively via IPC events — they have no direct access to the host system except through kernel-mediated host functions.
## Project Layout
```
my-capsule/
├── Capsule.toml # Manifest (name, version, capabilities, interceptors)
├── Cargo.toml # Rust crate (lib crate, cdylib)
└── src/
└── lib.rs # Capsule logic
```
## Capsule.toml
```toml
[package]
name = "my-capsule"
version = "0.1.0"
description = "Short description of what this capsule does"
authors = ["Your Name"]
astrid-version = ">=0.5.0"
[[component]]
id = "my-capsule"
file = "my_capsule.wasm" # underscores, not hyphens
type = "executable"
[capabilities]
# Filesystem access (read and/or write)
fs_read = ["home://data/"]
fs_write = ["home://data/"]
# IPC topics this capsule publishes to
ipc_publish = ["my.namespace.events.*"]
# IPC topics this capsule subscribes to (must match interceptor events)
ipc_subscribe = ["my.namespace.request.*"]
# Spawn host processes (list allowed binary names)
host_process = ["git", "cargo"]
# Allow outbound HTTP requests
allow_http = true
# Allow outbound network connections
allow_network = true
[imports]
# Interfaces this capsule requires from others
astrid = { session = "^1.0" }
[exports]
# Interfaces this capsule provides to others
my-namespace = { "my-interface" = "1.0.0" }
[[interceptor]]
event = "my.namespace.request.do-thing"
action = "tool_execute_do_thing" # must match generated arm name
[[interceptor]]
event = "system.v1.lifecycle.capsule_loaded"
action = "on_capsule_loaded"
priority = 50 # lower fires first; default 100
```
## Cargo.toml
```toml
[package]
name = "my-capsule"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
astrid-sdk = "0.5.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
uuid = { version = "1", features = ["v4"] }
```
## lib.rs — Skeleton
```rust
#![deny(unsafe_code)]
#![deny(clippy::all)]
use astrid_sdk::prelude::*;
use astrid_sdk::schemars;
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct MyCapsule;
#[capsule]
impl MyCapsule {
/// Run on first install. Set up VFS directories, write config, etc.
#[astrid::install]
pub fn on_install(&self) -> Result<(), SysError> {
astrid_sdk::fs::create_dir("home://data/my-capsule")?;
Ok(())
}
/// Run when capsule is upgraded to a new version.
#[astrid::upgrade]
pub fn on_upgrade(&self) -> Result<(), SysError> {
Ok(())
}
/// Handle an IPC event. The action name must match the `action` field
/// in Capsule.toml. InterceptResult controls the middleware chain:
/// Continue(payload) — next interceptor sees the (optionally mutated) payload
/// Final(payload) — chain stops, payload is the final result
/// Deny { reason } — chain stops, event is rejected
#[astrid::interceptor("on_some_event")]
pub fn on_some_event(&self, payload: Vec<u8>) -> Result<InterceptResult, SysError> {
let _msg: serde_json::Value = serde_json::from_slice(&payload)?;
Ok(InterceptResult::Continue(payload))
}
}
```
## SDK Macros
### `#[astrid::tool("tool_name")]`
Sugar for `#[astrid::interceptor]` with tool IPC conventions baked in. Generates:
- `tool_execute_<tool_name>` — handles `tool.v1.execute.<tool_name>`, publishes result to `tool.v1.execute.<tool_name>.result`
- `tool_describe` — handles `tool.v1.request.describe`, returns all tool JSON schemas
The function receives a strongly-typed args struct and returns `Result<String, SysError>`:
```rust
#[derive(Debug, Default, Deserialize, schemars::JsonSchema)]
pub struct MyArgs {
/// The thing to process (shown to the LLM as the parameter description)
pub input: String,
}
#[astrid::tool("my_tool")]
pub fn my_tool(&self, args: MyArgs) -> Result<String, SysError> {
Ok(format!("processed: {}", args.input))
}
```
Add the Capsule.toml interceptors:
```toml
[[interceptor]]
event = "tool.v1.execute.my_tool"
action = "tool_execute_my_tool"
[[interceptor]]
event = "tool.v1.request.describe"
action = "tool_describe"
```
And capabilities:
```toml
ipc_publish = ["tool.v1.execute.*.result", "tool.v1.response.describe.*"]
ipc_subscribe = ["tool.v1.execute.my_tool", "tool.v1.request.describe"]
```
### `#[astrid::interceptor("action_name")]`
Raw event handler. Action name matches the `action` field in `[[interceptor]]`. Receives raw bytes, returns `InterceptResult`:
```rust
#[astrid::interceptor("my_handler")]
pub fn my_handler(&self, payload: Vec<u8>) -> Result<InterceptResult, SysError> {
Ok(InterceptResult::Final(b"done".to_vec()))
}
```
### `#[astrid::install]` / `#[astrid::upgrade]`
Lifecycle hooks. `install` runs once on `astrid capsule install`. `upgrade` runs on version update. Both have signature `fn(&self) -> Result<(), SysError>`.
## IPC Patterns
### Fire and forget
```rust
use astrid_sdk::ipc;
ipc::publish("my.namespace.event", b"{\"key\":\"value\"}")?;
```
### Request / response (correlation ID)
```rust
use astrid_sdk::ipc;
use uuid::Uuid;
let correlation_id = Uuid::new_v4().to_string();
let response_topic = format!("my.namespace.response.{correlation_id}");
ipc::subscribe(&response_topic)?;
ipc::publish("my.namespace.request", serde_json::to_vec(&serde_json::json!({
"response_topic": response_topic,
"data": "..."
}))?)?;
// Poll for response (500ms timeout)
let response = ipc::recv_bytes(&response_topic, 500)?;
ipc::unsubscribe(&response_topic)?;
```
### Trigger hook (fan-out, collect all responses)
Used to broadcast to all interceptors on a topic and collect their payloads:
```rust
use astrid_sdk::hooks;
let results: Vec<Vec<u8>> = hooks::trigger("tool.v1.request.describe", b"")?;
```
## VFS
All file paths are UTF-8 strings. Scheme `home://` maps to the calling principal's home directory.
```rust
use astrid_sdk::fs;
// Read
let content = fs::read_to_string("home://data/config.json")?;
// Write (requires fs_write capability)
fs::write("home://data/output.txt", b"hello")?;
// Directory
fs::create_dir("home://data/my-dir")?;
let entries = fs::read_dir("home://data/")?;
for entry in entries {
println!("{}", entry.file_name());
}
// Check existence
if fs::exists("home://data/file.txt")? { ... }
```
## KV Store
Per-capsule, per-principal key-value store. Scoped automatically by the kernel.
```rust
use astrid_sdk::kv;
kv::set("my-key", b"value")?;
let val = kv::get("my-key")?; // Option<Vec<u8>>
kv::delete("my-key")?;
let keys = kv::list_keys("prefix:")?;
```
## Logging
```rust
use astrid_sdk::log;
log::info("capsule started")?;
log::warn("something unusual")?;
log::error("something failed")?;
// Or with format:
log::info(format!("processed {} items", count))?;
```
## Reading WIT Interfaces at Runtime
WIT files are installed to `home://wit/` during `astrid init`. Use them to understand message schemas:
```rust
let session_wit = astrid_sdk::fs::read_to_string("home://wit/session.wit")?;
```
Available interfaces: `session.wit`, `tool.wit`, `llm.wit`, `prompt.wit`, `context.wit`, `hook.wit`, `registry.wit`, `spark.wit`, `types.wit`.
## Build
```bash
cargo build --target wasm32-unknown-unknown --release
# Output: target/wasm32-unknown-unknown/release/my_capsule.wasm
```
Requires the target:
```bash
rustup target add wasm32-unknown-unknown
```
## Install
Use the `install_capsule` system tool, or from the CLI:
```bash
astrid capsule install ./path/to/capsule
astrid capsule install @github-org/capsule-repo
```
## Common Errors
| Error | Fix |
|---|---|
| `capability denied: fs_write` | Add `fs_write = ["home://..."]` to `[capabilities]` |
| `ipc: topic not subscribed` | Add topic to `ipc_subscribe` in `[capabilities]` |
| `interceptor action not found` | Check `action` in `[[interceptor]]` matches generated arm name |
| `wasm trap: unreachable` | Panic in guest — check for unwrap/expect on error paths |
| `boot validation: unsatisfied import` | Install the capsule that exports the required interface |
Ver en GitHub