creating a plugin
webassembly plugins, generic app-defined hooks, and the permissions that bound them.
the plugin model
plugins are webassembly modules. the app integration chooses each hook’s name, payload format, and required capability; fpui transports the name and opaque bytes without interpreting them.
transform(hook, payload)returns rewritten bytes, orNoneto keep the current payload. the host chains granted plugins in registration order, so each receives the previous output.observe(hook, payload)receives the same inputs and returns nothing. observing a hook cannot replace the payload the host uses.
a plugin implements either dispatcher or both, and can ignore names it does not handle. fpui defines no built-in message, document, or lifecycle vocabulary. another app’s hooks only exist when its integration provides them.
sdk and build
create a rust library with cdylib output. replace the sdk path with the path to your engine checkout:
[package]
name = "example-plugin"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
fpui-plugin-sdk = { path = "../engine/crates/fpui-plugin-sdk" } in src/lib.rs, define a dispatcher. this example uppercases ascii bytes for an illustrative text hook and leaves every other hook unchanged:
fpui_plugin_sdk::plugin! {
transform: |hook: &str, payload: &[u8]| match hook {
"text" => Some(payload.to_ascii_uppercase()),
_ => None,
},
} build the module from the plugin crate:
rustup target add wasm32-unknown-unknown
cargo build --release --target wasm32-unknown-unknown the result is target/wasm32-unknown-unknown/release/example_plugin.wasm. the app must supply the matching hook and consume its transformed output. other languages can target the wasm abi directly.
javascript authoring
the experimental js feature in fpui-wasm runs javy-compiled javascript under the same wasmi interpreter. hook dispatch, observation, and a shared engine backend are implemented. this is a separate library path: the agent’s FPUI_PLUGINS configuration still loads the native wasm abi shown here.
the javascript backend currently exposes no capability host imports. it can transform its input, but an observer cannot call app services. see the roadmap for the remaining integration work.
permissions
capabilities are app-defined strings. the manifest requests them, and the host accepts only a granted subset of that request. trying to grant an unrequested capability is rejected; a plugin without the hook’s required grant is never called through it.
the app integration owns the meaning of a grant and what it includes in the payload. permission to observe an event does not itself define which content that event may carry. sensitive fields must stay behind the appropriate app-defined capability.
the native wasm host gives each plugin its own instance, a 64 mib linear-memory limit, a 16 mib transform-output limit, and a default budget of 50 million fuel units per call. initial memory allocation and later growth are both limited. a trap, exhausted budget, or invalid result leaves the current transform payload unchanged.
loading plugins
set FPUI_PLUGINS to a JSON file containing module paths, manifests, and grants:
[
{
"wasm": "/absolute/path/example_plugin.wasm",
"manifest": {
"api_version": 1,
"id": "example-plugin",
"name": "example plugin",
"version": "0.1.0",
"author": "you",
"license": "GPL-3.0-or-later",
"source": "https://example.invalid/example-plugin",
"kind": "plugin",
"capabilities": [
"text",
"lifecycle"
]
},
"granted": [
"text"
]
}
] these names are examples for an integration that defines them. this plugin requests text and lifecycle, but only receives hooks guarded by text. paths and metadata are placeholders to replace.
the current native event bridge copies the configured call arguments, serializes the strings into a JSON array, and queues an observe call on the plugin worker. it leaves the original native call’s data unchanged. using the host’s transform chain requires an integration that consumes the returned bytes.
the rust sdk’s transform-output buffers have no explicit free export. repeated successful transforms can grow guest memory until the host’s cap is reached; reloading the plugin resets that instance.