Building extensions

An extension is a sealed, signed .hexon package that adds behaviour to one petal: a simulated fleet of movers and sensors, an adapter that turns device traffic into readings, a connector that pushes query results to an analytics target, or anything else that fits the same grants. It runs as a WebAssembly component. It has no network socket, file, clock or SQL string of its own. Everything it can do is declared in its manifest, consented to by the person running the app, and checked again on every call.

Preview limits, up front. The authoring command line, fe-ext, is built from a checkout of the repository and is not published as a package yet. Extensions are written in Rust only. Running one in the desktop app needs a source build with the plugin-host cargo feature and the environment variable FE_PLUGIN_HOST_ALLOW=1. The released preview builds answer host-off.

What you need

  • A checkout of the repository and a Rust toolchain.
  • The WebAssembly target: rustup target add wasm32-wasip2.
  • fe-ext, built from tools/fe-ext in the checkout:
cargo build --release -p fe-ext
# the binary is target/release/fe-ext

1. Scaffold

fe-ext new general my-ext

The kind is one of general, simulation, device-adapter or bi-connector. This writes ./my-ext/ with a Rust guest crate, an extension.json manifest, a fixture at fixtures/basic.json and a journey test. It refuses to write anything if its own manifest fails validation.

The manifest is the whole capability surface. The host grants nothing it does not declare:

{ "manifest_version": 2, "id": "local.my-ext", "version": "0.1.0", "name": "my-ext",
  "kind": "general", "contract": "fractalengine:plugin@2.0.0", "binding": "petal",
  "scene": { "read": true, "write": ["set_property"], "property_scope": ["my_ext.*"] },
  "tick": { "cadence_ms": 1000, "max_ops": 16, "max_readings": 16, "fuel": 200000 } }

Four rules explain most validation errors:

  • Absent grants nothing. A scope list that is present but empty is refused as empty-scope; omit the field instead.
  • An extension that writes must declare tick. Writes are staged and only a tick flushes them.
  • A wildcard such as * must be written out. It is never implied.
  • The id is the signed identity. It must equal the id the package is signed under.

2. Write the guest

The guest SDK lives in tools/fe-ext/guest. Implement its Extension trait (only on_tick is required) and call extension!(YourType) once. A guest never saves anything directly: it stages changes, and the host commits everything staged in one tick as a single write. The worked example, extensions/fleet-sim, is a complete guest of about 230 lines.

3. Validate, build, run in the sandbox

fe-ext validate --project my-ext
fe-ext build --project my-ext        # writes my-ext/dist/component.wasm
fe-ext run --sandbox --project my-ext --fixture my-ext/fixtures/basic.json --out trace.json

build also runs the component through the host’s install checks, so a missing export or a forbidden import fails here and not at install.

The sandbox runs your component in the real host against an in-memory mock scene, with a deterministic clock, and prints a fingerprint. Two runs of the same fixture must print the same fingerprint; if they do not, the guest depends on wall-clock time or iteration order. The sandbox does not exercise bus commands or outbound HTTP completions: those are answered not-available.

4. Sign and package

fe-ext keygen publisher.key                       # prints your publisher id (did:key); never overwrites
fe-ext pack --project my-ext --key publisher.key  # my-ext/dist/local.my-ext-0.1.0.hexon
fe-ext inspect my-ext/dist/local.my-ext-0.1.0.hexon

pack seals and signs in one step and produces a sealed, signed .hexon. inspect prints the identity, the signers and the capability digest.

5. Install and run it in the app

This part needs the source build described at the top.

cargo run -p fractalengine --features plugin-host
# with FE_PLUGIN_HOST_ALLOW=1 set in the environment

Trust. The app installs a package only if its publisher is listed in the app’s trusted_publishers.toml. Each entry is a label and the publisher’s ed25519 public key:

[[publisher]]
key_id = "my-team"
public_key = "<64 hex characters>"

Sources. Where the app looks for packages is set in extension_sources.json. A source is one of three kinds, named by its kind field: a local path (local), a signed static index whose signing keys you pin (static), or a registry (protocol_v1). The exact file locations are in the governance reference below.

Consent. Installing a package opens a consent dialog that lists every capability the package declares. Nothing runs until you consent. Consent covers exactly that list: if a later version changes any grant, you are asked again, and the running version stays on until you accept. Consent is per device and is never inherited from another peer.

Enable and bind. After consent, enable the extension and bind it to a petal, a fractal or a whole verse. A binding grants nothing by itself: the extension runs only on devices that installed it, consented, and accepted it.

REST and MCP never install an extension, so an AI agent cannot install one for you.

Go deeper

The full references are in the repository:

The repository marks these documents as drafts, so check them against the code before depending on a detail.