Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Plugins & Extensions

Kael ships an extension system with a contribution-point architecture. Extensions run out-of-process and can target one of two execution models: a sandboxed WASM module, or an external process that speaks the extension RPC protocol. The host loads extensions from a manifest, mediates their capabilities through a permission broker, and dispatches commands and notifications to them.

Defining a manifest

Build a PluginManifest with the builder. The positional arguments are id, name, version, api_version, entry_point, and execution_model; contribution points and capabilities are added fluently.

#![allow(unused)]
fn main() {
use kael::{ContributedCommand, ExecutionModel, PluginManifest};

let manifest = PluginManifest::builder(
    "com.example.mock",   // id
    "Mock Plugin",        // display name
    "1.0.0",              // plugin version
    "1.0.0",              // host API version it targets
    "mock.wasm",          // entry point
    ExecutionModel::Wasm,
)
.command(ContributedCommand {
    id: "mock.hello".to_string(),
    title: "Say Hello".to_string(),
    keybinding: None,
})
.build()?;
}

Manifests can also be loaded from disk with PluginManifest::from_json, PluginManifest::from_toml, or PluginManifest::load(path). Manifest validation rejects empty, padded, control-character, overly long, or non-portable IDs/names/versions, unsafe entry-point text, duplicate command or panel IDs, menu items that reference missing commands, and non-object settings schemas before the extension is loaded.

Loading and activating

ExtensionHostRuntime owns the installed extensions for an app. Load a manifest, then activate it — activate_with_broker runs the extension's capability requests through a PermissionBroker first.

#![allow(unused)]
fn main() {
use kael::{ExtensionHostRuntime, PermissionBroker};

let mut runtime = ExtensionHostRuntime::new(&extensions_dir, "my-app");
runtime.load(manifest)?;

let broker = PermissionBroker::new();
for ext in runtime.all().iter().map(|e| e.manifest.id.clone()).collect::<Vec<_>>() {
    runtime.activate_with_broker(&ext, &broker)?;
}
}

Other runtime operations: load_from_directory (dev mode), install_from_path, uninstall, activate / deactivate, unload, send_command, broadcast_notification, and all (returns &ExtensionInfo with manifest, is_active, process_id, load_path, dev_mode).

Crash and restart policy

Extension hosts should keep crash state outside the extension process. Use CrashPolicy::validate, CrashRecord::new_checked, record_crash_checked, should_restart_checked, and next_restart_delay_checked when wiring restart loops. The checked helpers reject invalid extension IDs, zero-delay restart loops, non-finite backoff values, and backoff factors below 1.0; extremely large restart delays saturate to u64::MAX.

#![allow(unused)]
fn main() {
use kael::{CrashPolicy, CrashRecord};

let policy = CrashPolicy::default();
policy.validate()?;

let mut crashes = CrashRecord::new_checked("com.example.mock")?;
crashes.record_crash_checked(&policy)?;

if crashes.should_restart_checked(&policy)? {
    let delay_ms = crashes.next_restart_delay_checked(&policy)?;
    // Schedule the extension restart after delay_ms.
}
}

Contribution points

Extensions extend the host by contributing entries through the builder:

Builder methodContributes
.command(ContributedCommand)A command (id, title, optional keybinding)
.menu_item(ContributedMenuItem)A menu entry (target_menu, label, command_id)
.panel(ContributedPanel)A panel/view (id, title, default_position)
.settings_schema(json)A JSON schema for configurable settings
.capability(Capability)A requested capability (e.g. Capability::Notification)

Panels position with PanelPosition::{Left, Right, Bottom, Floating}.

Execution models & RPC

ExecutionModel::Wasm runs the entry point as a sandboxed WebAssembly module. ExecutionModel::ExternalProcess launches a separate binary that connects over the platform transport and exchanges messages with the host.

The host and an external extension first complete a handshake (ExtensionHandshake, version-checked against EXTENSION_RPC_VERSION), then exchange a typed envelope:

DirectionTypeKey variants
host → extExtensionRequestActivate, Deactivate, Shutdown, GetContributions, ExecuteCommand { command_id, args }
ext → hostExtensionResponseAck, Contributions(Contributions)
host → extExtensionNotificationSettingsChanged { key, value }

Broadcast a notification to every active extension:

#![allow(unused)]
fn main() {
use kael::ExtensionNotification;

runtime.broadcast_notification(ExtensionNotification::SettingsChanged {
    key: "theme".to_string(),
    value: serde_json::json!("dark"),
});
}

For the complete contract, see the ExtensionRuntime, ExtensionBroker, and ExternalExtensionProcess API documentation. The consolidated Astryx showcase demonstrates the UI primitives used to present extension state, commands, and logs; extension lifecycle and transport behavior are covered by the process integration tests so the example gallery does not duplicate security-sensitive runtime code.